UICollectionViewでドット絵表示

![Xcode-13.1](https://img.shields.io/badge/Xcode-13.1-brightgreen) 

## はじめに
WWDC 2019 で紹介された `UICollectionView` でシュバババと並べ替えが起きるアニメーションをみてやってみたい！と思った人は多いと思います（[Advances in UI Data Sources](https://developer.apple.com/videos/play/wwdc2019/220/)の 22:00 ~ くらいのやつ）。

ドット絵表示して切り替えるときに使うとかっこいいんじゃないかと思いこういうのを作ってみました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">CollectionViewでドット絵🤪 <a href="https://t.co/jtR5o1oSTF">pic.twitter.com/jtR5o1oSTF</a></p>&mdash; am10 (@am103141592) <a href="https://twitter.com/am103141592/status/1560977009484124160?ref_src=twsrc%5Etfw">August 20, 2022</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></blockquote>

## ドット絵表示
まず `UICollectionView` を使って 32 * 32 のキャンバスを作ってそこにドット絵を表示します。

Compositional Layouts と Diffable Data Sources どちらもあまり理解してないので下記記事参考にさせていただきました。

* [Compositional LayoutとDiffable Data Sourceを使ってiOSアプリのつくれぽ詳細画面を実装する](https://techlife.cookpad.com/entry/2020/12/24/130000)
* [Diffable Data Sourceでセルをリロードする方法](https://qiita.com/hironytic/items/8a3a73e571bd047124a3)
* [【Swift】UICollectionViewCompositionalLayoutを使って数種類のカスタムレイアウトを実装してみる](https://qiita.com/ddd503/items/205002b44b7a22e5ba13)
* [時代の変化に応じて進化するCollectionView ~Compositional LayoutsとDiffable Data Sources~](https://qiita.com/shiz/items/a6032543a237bf2e1d19)

### キャンバス作成
下記のように 32 * 32 のキャンバスを作成します（ドット打つの大変だったので 16 * 16 のがいいかもです）。

```swift
private enum Section {
    case main
}

/// サイズは256*256にしておく
@IBOutlet private weak var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Int>!

private func makeSection() -> NSCollectionLayoutSection {
    let itemCount = 32 // 横に並べる数
    let itemSpacing: CGFloat = 0 // セル間のスペース
    let dotSize: CGFloat = 8 // ドットのサイズ

    let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .absolute(dotSize),
                                                        heightDimension: .absolute(dotSize)))
    let items = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1.0),
                                                                        heightDimension: .fractionalHeight(1.0)),
                                                    subitem: item,
                                                    count: itemCount)
    items.interItemSpacing = .fixed(itemSpacing)
    let group = NSCollectionLayoutGroup.vertical(layoutSize: .init(widthDimension: .fractionalWidth(1.0),
                                                                    heightDimension: .absolute(dotSize)),
                                                    subitems: [items])

    let section = NSCollectionLayoutSection(group: group)
    return section
}

collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(section: makeSection())
dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
    (collectionView: UICollectionView, indexPath: IndexPath, item: Int) -> UICollectionViewCell? in
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
    cell.backgroundColor = .systemGreen
    return cell
}

let items = (0...1023).map { Int($0) }
var snapshot = NSDiffableDataSourceSnapshot<Section, Int>()
snapshot.appendSections([.main])
snapshot.appendItems(items, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: false)
```

こんな感じです。

<img width="300" alt="dot1" src="https://user-images.githubusercontent.com/34936885/185887058-37a93fdf-2a03-4e8b-a04e-e3491fb40316.png">

### ドット絵表示
使う色の文だけ `Dot` を定義します。

```swift
enum Dot {
    case chocolate1
    case chocolate2
    case cracker1
    case cracker2
    case outline
    case background

    var color: UIColor {
        switch self {
        case .chocolate1:
            return .init(red: 93/255, green: 63/255, blue: 48/255, alpha: 1.0)
        case .chocolate2:
            return .init(red: 60/255, green: 42/255, blue: 33/255, alpha: 1.0)
        case .cracker1:
            return .init(red: 236/255, green: 193/255, blue: 131/255, alpha: 1.0)
        case .cracker2:
            return .init(red: 164/255, green: 137/255, blue: 97/255, alpha: 1.0)
        case .outline:
            return .init(red: 96/255, green: 96/255, blue: 96/255, alpha: 1.0)
        case .background:
            return .systemGreen
        }
    }
}
```

こんな感じで 1024 の配列でドット絵を定義します。

```swift
let mushroom: [Dot] = [
    .background, .background, .background, .background, .background, .background, .background, .background,
    .background, .background, .background, .background, .outline, .outline, .outline, .outline,
    .outline, .outline, .outline, .outline, .background, .background, .background, .background,
    .background, .background, .background, .background, .background, .background, .background, .background,

    :
    :
    :
]
```

あとは下記のようにしてドット絵を表示するだけです！

```swift
private var dataList: [Dot] = mushroom

dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
    (collectionView: UICollectionView, indexPath: IndexPath, item: Int) -> UICollectionViewCell? in
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
    cell.backgroundColor = self.dataList[item].color
    return cell
}
```

こんな感じです。

<img width="300" alt="dot2" src="https://user-images.githubusercontent.com/34936885/185887101-d94965ea-07db-40b6-a530-0cac6043b326.png">

## ドット絵切り替え
ドット絵が表示できたのであとは複数のドット絵の切り替え処理です。

もう１つドット絵データを作成します。

```swift
let bambooshoot: [Dot] = [
    .background, .background, .background, .background, .background, .background, .background, .background,
    .background, .background, .background, .background, .background, .background, .background, .outline,
    .outline, .background, .background, .background, .background, .background, .background, .background,
    .background, .background, .background, .background, .background, .background, .background, .background,
    :
    :
    :
]
```

切り替え用フラグを用意してデータ生成の処理を修正します。

```swift
private var mode = 0

let items = (0...mushroom.count-1).map { Int($0) }
dataList = .init(repeating: .background, count: items.count)
items.forEach { item in
    switch mode {
    case 1:
        dataList[item] = bambooshoot[item]
    case 2:
        dataList[item] = mushroom[item]
    default:
        dataList[item] = .background
    }
}
```

あとはボタン押下時に更新処理を実行するだけです！

```swift
@IBAction func change(_ sender: Any) {
    mode += 1
    if mode > 2 {
        mode = 0
    }

    var snapshot = dataSource.snapshot()
    let allItems = snapshot.itemIdentifiers
    let items = allItems.shuffled()
    snapshot.deleteItems(allItems)
    snapshot.appendItems(items, toSection: .main)
    snapshot.reloadItems(items)

    items.enumerated().forEach { item in
        switch mode {
        case 1:
            dataList[item.element] = bambooshoot[item.offset]
        case 2:
            dataList[item.element] = mushroom[item.offset]
        default:
            dataList[item.element] = .background
        }
    }

    dataSource.apply(snapshot, animatingDifferences: true)
}
```

## おまけ
SpriteKit の SKEmitterNode（爆炎部分）と組み合わせてこんなこともできます:boom:

（再生するたけのこ）

<img width="300" alt="ex" src="https://user-images.githubusercontent.com/34936885/185887231-70363517-746c-4194-8ebf-9f3a1cbd1daa.gif">

爆散と再生処理はこんな感じです。

```swift
/// 爆散処理
func shuffleDot() {
    var snapshot = dataSource.snapshot()
    let items = snapshot.itemIdentifiers
    snapshot.deleteItems(items)
    snapshot.appendItems(items.shuffled(), toSection: .main)
    dataSource.apply(snapshot, animatingDifferences: true)
}

/// 再生処理
func resetDot() {
    var snapshot = dataSource.snapshot()
    let items = snapshot.itemIdentifiers
    snapshot.deleteItems(items)
    snapshot.appendItems(items.sorted(by: { $1 > $0 }), toSection: .main)
    dataSource.apply(snapshot, animatingDifferences: true)
}
```

## おわりに
みんなも `UICollectionView` を使ってドット絵をシュバババと表示しよう:sunglasses: