今すぐ使えるDiffable Data Sources

## 初めに

iOSのレイアウト作成には`UITableView`がよく使われています。しかし、その構造を更新するときはちゃんとした実装が難しく、`reloadData`に頼ってしまう人が少なくありません。

それをより簡単にするAPIがAppleに公開されているが、まだまだ普及率が上がらない様子です。

今回の記事でその既存の実装に入れやすい使い方を簡単に紹介します。
`UITableView`についての話になりますが、`UICollectionView`と`NSCollectionView`向けのAPIも公開されていて、使い方はさほど変わりません。

![NoReloadData.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/652001/ae47c05b-17ab-f87d-1f15-b4dea85bc531.gif)

## データ構造を準備する

[`UITableViewDiffableDataSource`](https://developer.apple.com/documentation/uikit/uitableviewdiffabledatasource)はセクションとセルのデータ構造をベースに、テーブルの構造の変更を検知して自動的に適応します。

まずはそのデータ構造を準備します。これに入れる要素によって`UITableViewDiffableDataSource`が算出する変更が決まります。もし、この構造にセルの文言などを入れればそれの変更も検知することができます。ただ、削除 ＋ 追加と解釈されるので、セルがリロードされます。これが望ましくないものとして、今回は要素としてidのみを入れます。セル内容の更新はまた別のやり方を合わせて使います。

もう一つの注意点は、セクションとセルが**ユニークであること**が必要となります。

```swift
struct SectionIdentifier: Identifiable, Hashable {
    let id: Int
}

struct RowIdentifier: Identifiable, Hashable {
    let id: IndexPath
}
```

## 既存のUITableViewDataSourceを消す

`UITableViewDiffableDataSource`は`UITableViewDataSource`のプロトコルに準拠しているのでVCなどで実装されているものは要らなくなります。

- `numberOfSectionsInTableView:`
- `tableView:numberOfRowsInSection:`

この2つは完全に代替されるので削除しましょう。

`tableView:cellForRowAtIndexPath:`も不要になって、その役割は`UITableViewDiffableDataSource`の`init`で渡されるクロージャが果たします。
ただ、ここであえて移動せずに、生成とセル設定、2つの関数に分けます。そうすることで、セル内容の更新にそれを再利用できることになります。

```swift
func dequeueCell(forIndexPath indexPath: IndexPath, row: RowIdentifier) -> UITableViewCell {
    tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
}

func update(cell: UITableViewCell, row: RowIdentifier) {
    cell.textLabel?.text = "Row \(row.id.row) in section \(row.id.section)"
}
```

それ以外のプロトコル関数の実装があれば、UITableViewDiffableDataSourceのサブクラスを定義してそこに移動します。

## UITableViewDiffableDataSourceを生成

ここでいよいよ`UITableViewDiffableDataSource`を生成します。lazyでVCのプロパーティにします。initの引数としてtableViewが渡されるときは自動的にdataSourceに設定されるのでそれ以外の設定が要りません。

```swift
lazy private(set) var diffableDataSource: UITableViewDiffableDataSource<SectionIdentifier, RowIdentifier> = UITableViewDiffableDataSource(tableView: tableView) { [weak self] (tableView, indexPath, row) -> UITableViewCell? in
    guard let self = self else { return nil }
    
    let cell = self.dequeueCell(forIndexPath: indexPath, row: row)
    self.update(cell: cell, row: row)
    
    return cell
}
```

## テーブル構造を指定します

テーブル構造全体を設定したスナプショットの`NSDiffableDataSourceSnapshot`を作ります。スナプショットを作成して、セクションとセルを追加したらdiffableDataSourceにapplyするだけです。

例として、ランダムで0\~7個のセルを持っているセクションを1\~5個を作ります。

```swift
private func remakeStructure() {
    let sectionCount = (1...5).randomElement()!
    
    var snapshot = NSDiffableDataSourceSnapshot<SectionIdentifier, RowIdentifier>()
    (0..<sectionCount).forEach { sectionIndex in
        let section = SectionIdentifier(id: sectionIndex)
        snapshot.appendSections([section])
        
        let rowCount = (0...7).randomElement()!
        
        let rows = (0..<rowCount).map { rowIndex in
            RowIdentifier(id: IndexPath(row: rowIndex, section: sectionIndex))
        }
        
        snapshot.appendItems(rows, toSection: section)
    }
    diffableDataSource.apply(snapshot)
}
```

今度、テーブルを更新しているときは現在の構造をdiffableDataSource.snapshot()からもらえて、リロードや入れ替えなどを手動で設定することができます。
変更したスナプショットあるいは新しく作ったスナプショットをもう一度applyするだけで自動的にただしくアニメーション付きで適応されます。

## セルのコンテンツを更新する

あえて内容の更新が反映されないように作ったので、もちろん、セルの内容が更新されない。

それを解決するにはスナプショットを適応する直後に、表示中のセルを取得してチャチャッと事前に準備したセル設定の関数にかけるだけです。セクションヘッダーなどの変更は同じようなやり方でできるので割愛します。

```swift
tableView.beginUpdates()
tableView.indexPathsForVisibleRows?.forEach { indexPath in
    guard let cell = tableView.cellForRow(at: indexPath),
        let row = diffableDataSource.itemIdentifier(for: indexPath)
        else { return }
    update(cell: cell, row: row)
}
tableView.endUpdates()
```

なお、beginUpdates + endUpdatesで包んであげると、セルの高さが変わった場合は正しく反映されます。

## iOS 12以下はどうすればいい？

Appleが提供している`UITableViewDiffableDataSource`はiOS 13.0以上を必要としているが、同じAPIを低バージョンのOSで使えるライブラリがあります。

[ra1028/DiffableDataSources](https://github.com/ra1028/DiffableDataSources) (iOS 9.0+ / macOS 10.11+ / tvOS 9.0+)

実際に使い方がまったく一緒で、クラスとデータ構造の最初の頭文字があるかないかだけの違いになります。


---
今回紹介しましたソースコードはGistにまとめていますので、ぜひ見てみてください。

[https://gist.github.com/auramagi/f6b6b4a38f502b983a0c37dd3b262a6e](https://gist.github.com/auramagi/f6b6b4a38f502b983a0c37dd3b262a6e)