Diffable DataSource 入門

## 概要

iOS 13 から `UICollectionView` / `UITableView` に表示するデータを管理する方法として `UICollectionViewDiffableDataSource` / `UITableViewDiffableDataSource` が登場しました。 Diffable DataSource を使うことにより以前までの方法と比べて宣言的に書くことができ、また美しいアニメーションのサポートも受けることができますが、一見すると登場人物が多く複雑に見えます。この記事では、 Diffable DataSource を触ったことがない人でもざっくりとその使い方が理解できるように Diffable DataSource の基礎知識をまとめます。そのための例として Todo アプリを取り上げ、以下の順で作っていきます。その過程で一通り Diffable DataSource の使い方が理解できるようにします。

- Todo アプリの最小構成を作る
- Todo を削除できるようにする
- Todo を編集できるようにする
- セクションヘッダを追加する
- Done になっていない　Todo のみ表示できるようにする

この記事では iOS 15 以上の `UICollectionView` を例に使いますが、 `UITableView` を使う場合や iOS 15 未満の場合でも全体的な考え方は変わらないと思います。また、 `UICollectionView` では同じことを複数の方法で実現できる場合がありますが、今回は概略を掴むことが目的なので、網羅的に紹介することなくもっとも新しかったり基本的だと思われる方法を使うことにします。

記事中の動作検証は Xcode 14 Beta 5 を使って行っています。

## Todo アプリの最小構成を作る

最初に Todo アプリの最小構成を作っていきます。まず、最小構成に必要な登場人物を列挙しておきます。

- `UICollectionView`
- Section
- Item
    - ItemIdentifier
- `UICollectionView.CellRegistration<UICollectionViewCell, Item>`
- `UICollectionViewDiffableDataSource<Section, ItemIdentifier>`
- `NSDiffableDataSourceSnapshot<Section, ItemIdentifier>`

以下で、それぞれの役割を説明しながら実装していきます。

### `UICollectionView`

`UICollectionView` はなんらかのデータの集まりを表示するための view です。今回は `UICollectionView` を使って Todo のリストを表示するので、当然 `UICollectionView` を画面上に貼っておく必要があります。 `UICollectionView` を作る上で、例えば `UICollectionViewCompositionalLayout` を使ってレイアウトを指定する必要がありますが、この記事の主目的はレイアウトではなく Diffable DataSource について理解することなので、 自分でレイアウトを組むのではなく iOS 14 以降で標準で用意されている `UICollectionLayoutListConfiguration` を使います。 `UICollectionLayoutListConfiguration` は table view のようなリスト形式の view を表示するための configuration です。これまでに馴染みがない場合でもこの記事を読む上ではそういうものがあるんだと思っておいていただければ大丈夫ですが、詳しくは以下のセッションで詳しく紹介されています。

https://developer.apple.com/videos/play/wwdc2020/10026

まずは以下の実装を追加して `UICollectionView` を画面に表示します。この時点では view を貼っただけで DataSource と繋いでいないので何もデータは表示されません。
今回は `UICollectionView` をコードで生成していますが、 Storyboard で生成しても大丈夫です。いずれにしてもここまでは view の話であって DataSource には関係ありません。

```swift
final class TodoListViewController: UIViewController {
    private var collectionView: UICollectionView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureCollectionView()
    }
    
    private func configureCollectionView() {
        // collection view を初期化
        let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
            let configuration = UICollectionLayoutListConfiguration(appearance: .plain)
            return NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
        }
        collectionView = UICollectionView(frame: .null, collectionViewLayout: layout)
        
        // collection view を view の全面を覆う形で hierarchy に追加
        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
    }
}
```

### Section / Item / Item Identifier

DataSource を特徴づける型として Section / Item / Item Identifier があります。 

Section は CollectionView のセクションのことで、データの表示形式を変えたりデータをまとまりごとに分離したい場合にその数だけ作ります。今回はフラットな Todo リストにするので `main` という Section を１つだけ作ることにします。

Item は CollectionView の１つのセルに対応するデータです。今回は Todo を表示したいので Todo を表すデータ構造が Item になります。 Item Identifier は Item を一意に特定する id のことで、 Diffable DataSource においては非常に重要な役割を果たします。

`Todo` という `struct` を作り、これを `Identifieable` に準拠させます。 `Identifiable` の要求は  `id` というプロパティを持つことのみです。この `id` の型は `Hashable` であればなんでも大丈夫ですが、その型は associated type である `ID` に対応するため、これが `Todo.ID` というように参照できるようになります。

```swift
struct Todo: Identifiable {
    var id: UUID // Todo.ID が UUID のエイリアスになる
    var title: String
    var done: Bool
}

final class TodoListViewController: UIViewController {
    enum Section {
        case main
    }

    // ...
}
```

以上のこのコードではそれぞれ以下の役割を果たします。

- Section: `Section`
- Item: `Todo`
- Item Identifier: `Todo.ID` (`UUID`)

`Identifiable` の詳細についてはドキュメントを参照ください。

https://developer.apple.com/documentation/swift/identifiable

### `UICollectionView.CellRegistration<UICollectionViewCell, Item>`

DataSource を作成するために `CellRegistration` が必要なのでこちらを先に作ります。 `UICollectionView.CellRegistration<UICollectionViewCell, Item>` は、 Item が与えられたときにそれをどのように Cell の表示に反映するかを指定します。コードを見た方が早いと思うので先に載せてしまいます。

```swift
final class TodoListViewController: UIViewController {
    override func viewDidLoad() {
        // ...
        configureDataSource()
    }

    private func configureDataSource() {
        let todoCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Todo> { cell, indexPath, todo in
            var configuration = cell.defaultContentConfiguration()
            configuration.text = todo.title
            cell.contentConfiguration = configuration
            
            cell.accessories = [
                .checkmark(displayed: .always, options: .init(isHidden: !todo.done))
            ]
        }
    }
}
```

`CellRegistration` には Cell の設定をするためのクロージャを渡しますが、このクロージャの引数として Cell と Item が渡ってきます。それぞれの型は `CellRegistration` の型パラメータで指定していまます。今回は CollectionView に `UICollectionLayoutListConfiguration` を使っているので、これに合わせて Cell は `UICollectionViewListCell` にします。これは `UICollectionViewCell` のサブクラスで、リスト表示に使ええ、デフォルトで色々な要素がついてきて便利です。また、前の節で説明した通り今回の CollectionView における Item は `Todo` です。

`UICollectionViewListCell` は本題ではないので詳しい説明はしませんが、コードから `Todo` の `title` を表示していることや `done` が `true` の場合はチェックマークを表示していそうなことがわかると思います。この設定に使っている `contentConfiguration` や `accessories` は `UICollectionViewListCell` に特有のプロパティですが、自作の Cell を使う場合などもやることは同じで、受け取った Item の情報を必要に応じて Cell に反映します。


### `UICollectionViewDiffableDataSource<Section, ItemIdentifier>`

いよいよ `UICollectionViewDiffableDataSource` を追加していきます。 `DiffableDataSource` の役割は CollectionView に表示するデータを提供することです。と言っても、実装をしていく上で実際にデータを反映するのは次の節で紹介する `NSDiffableDataSourceSnapshot` なので、実装観点では `DiffableDataSource` にはデータの全体の流れの調整役のようなイメージを持っておくとわかりやすいかもしれません。

コードは以下のようになります。

```swift
final class TodoListViewController: UIViewController {
    private var dataSource: UICollectionViewDiffableDataSource<Section, Todo.ID>!
    private var repository: TodoRepository = .init()

    private func configureDataSource() {
        let todoCellRegistration = // ...

        // DataSource の生成
        dataSource = UICollectionViewDiffableDataSource(
            collectionView: collectionView, // DataSource と CollectionView の紐づけ
            cellProvider: { [weak self] collectionView, indexPath, todoID in // Cell を dequeue　して返却
                let todo = self?.repository.todo(id: todoID)
                return collectionView.dequeueConfiguredReusableCell(using: todoCellRegistration, for: indexPath, item: todo)
            }
        )
    }
}
```

まず、 `UICollectionViewDiffableDataSource` は型パラメータに Section と Item Identifier を取るので、それぞれここまでに定義した `Section` と `Todo.ID` を入れています。イニシャライザには DataSource を紐付けたい CollectionView と、 Cell を返す `cellProvider` を指定します。 `cellProvider` は Cell を dequeue して返すクロージャで、引数に Item Identifier が渡ってきます。この Identifier を元に Repository から Item を取得し、その Item と `CellRegistration` を組み合わせることで Cell を返すことができます。

ここで、実際の Todo データの提供元として `TodoRepository` を追加しています。もし本当に使うアプリならば Todo は API や DB から取得することになるでしょうが、今回は `TodoRepository` 初期化時にメモリ上に Todo を30個生成するようにしています。

```swift
final class TodoRepository {
    private var todos: [Todo] = (1...30).map { i in
        Todo(id: UUID(), title: "Todo #\(i)", done: false)
    }
    
    func todo(id: Todo.ID) -> Todo? {
        todos.first(where: { $0.id == id })
    }
}
```

### `NSDiffableDataSourceSnapshot<Section, ItemIdentifier>`

ここまで実装した状態でアプリを実行しても画面は真っ白なままです。ここまでのコードはあくまでデータの流れを作るものであって、まだ実際のデータがどこからも CollectionView に渡されていないからです。データを渡すには `NSDiffableDataSourceSnapshot` を使います。

具体的には、 `NSDiffableDataSourceSnapshot` の `appendSections` を呼ぶことで Section を追加し、さらに `appendItems` で Section に対して Item Identifier を追加します。 Item 自体ではなく Item Identifier を渡すことに注意する必要があり、今回は `Todo` の `ID` のみの配列を Repository に用意しています。作った Snapshot を `UICollectionViewDiffableDataSource` に `apply` することで CollectionView にデータが渡されます。

```swift
final class TodoRepository {
    // ...

    var todoIDs: [Todo.ID] { todos.map(\.id) }
}

final class TodoListViewController: UIViewController {
    override func viewDidLoad() {
        // ...

        applySnapshot()
    }

    private func applySnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Todo.ID>()
        snapshot.appendSections([.main])
        snapshot.appendItems(repository.todoIDs, toSection: .main)
        
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}
```

 `NSDiffableDataSourceSnapshot` には型パラメータでそれぞれ Section と Item Identifier を指定するので、指定した以外の型を渡すことはできず安全になっています。

アプリを実行してみると、想定通り Todo が表示されています。

![list.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/9670ee01-8b95-95fa-e7d1-f5cad70d5909.gif)

ここまでの実装は以下の gist にあります。

https://gist.github.com/maiyama18/0e950ca5f17e8cfe760daa8139df345a

### 最小構成のまとめ

- CollectionView / DataSource を特徴付ける型として Section と Item があり、 Item は自身を特定できる型である Item Identifier を持つ
- DataSource におけるデータ反映の流れを以下のように作る
    - `CellRegistration` に Item を Cell の表示に反映する処理を書く
    - `UICollectionViewDiffableDataSource` の `cellProvider` に Item Identifier が渡されてくるので、そこから Item を取得し、 その Item と `CellRegistration` を使って Cell を dequeue する処理を書く
- 実際のデータ反映をするために `NSDiffableDataSourceSnapshot` を生成し、 Section と Item Identifier を追加して `UICollectionViewDiffableDataSource` に `apply` する

### Item を Item Identifier として使うことについて

`UICollectionViewDiffableDataSource` の型パラメータには Item Identifier が使われているのに対し、 `CellRegistration` には Item が使われています。これは Cell を設定するために Item が必要なため自然ですが、この食い違いのために `UICollectionViewDiffableDataSource` の `cellProvider` の中で Item Identifier から Item へ変換する手間が生じています。

この問題を解決するために Item Identifier を Item そのものにしてしまうという手法があります。 Item Identifier は `Hashable` に準拠していれば大丈夫なので、 `Todo` を `Hashable` にして以下のような修正を加えればデータの流れが `Todo` で完結することになります。

```diff_swift
-struct Todo: Identifiable {
+struct Todo: Identifiable, Hashable {
    var id: UUID
    var title: String
    var done: Bool
}


final class TodoListViewController: UIViewController {
-   private var dataSource: UICollectionViewDiffableDataSource<Section, Todo.ID>!
+   private var dataSource: UICollectionViewDiffableDataSource<Section, Todo>!
  
    // ...   
}
```

これは一見楽なようですが、

- 後に紹介する `reconfigureItems` が動作しなくなる
- プロパティの値が変わった場合に Identifier が変わってしまうのでアニメーションが想定通り動作しない場合がある
- hash の計算に時間がかかるようになりパフォーマンスが悪くなる

というデメリットがあることから Apple は推奨していないので、もし新しく CollectionView を作る場合は Item Identifier は Item 自体とは異なる不変かつシンプルな型にしておいたほうが無難だと思います。ただし、もし Item 自体が不変かつシンプルであったり、そうでなくてもプロトタイピング目的であれば Item = Item Identifier の設計でも問題ないようです。詳しくは以下のドキュメントの Populate Snapshots with Lightweight Data Structures の節を参照ください。

https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/updating_collection_views_using_diffable_data_sources

## Todo を削除できるようにする

これから最小構成に機能を足しつつ Diffable DataSource の機能を理解していきますが、まずは Todo を削除できるようにしてみます。 `UICollectionLayoutListConfiguration` の `trailingSwipeActionsConfigurationProvider` に適切な `UIContextualAction` を渡して、その中で削除処理を実施することにより Cell を左にスワイプして削除できるよくある UI が実現できます。

先に削除処理の追加に必要なすべての差分を示します。

```diff_swift
final class TodoRepository {
    // ...

+   func removeTodo(id: Todo.ID) {
+       todos = todos.filter { $0.id != id }
+   }
}

final class TodoListViewController: UIViewController {
    // ...

    private func configureCollectionView() {
        let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
            var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
+           configuration.trailingSwipeActionsConfigurationProvider = { indexPath in
+               let deleteAction = UIContextualAction(style: .destructive, title: "削除") { [weak self] action, view, completion in
+                   guard let self = self,
+                         let todoID = self.dataSource.itemIdentifier(for: indexPath)
+                   else {
+                       completion(false)
+                       return
+                   }
+                   self.repository.removeTodo(id: todoID)
+                   self.applySnapshot()
+                   completion(true)
+               }
+               
+               return UISwipeActionsConfiguration(actions: [deleteAction])
+           }
            return NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
        }
    }

    // ...
}
```

`trailingSwipeActionsConfigurationProvider` は `indexPath` を引数にして `UISwipeActionsConfiguration` を返すクロージャを引数にとります。この `UISwipeActionConfiguration` に削除処理を実装した `UIContextualAction` を渡しています。

`UIContextualAction` の３つ目の引数のクロージャで削除処理を行なっています。注意すべき点として、 `trailingSwipeActionsConfigurationProvider` が Item Identifier ではなく `indexPath` を渡してくることです。最小構成を作る過程で分かったように基本的には Diffable DataSource を使った CollectionView の実装では Item Identifier が Item を特定するための手段になるのですが、一部の API では `indexPath` を元に処理を書かないといけないことがあります。このような場合には `UICollectionViewDiffableDataSource` の `itemIdentifier` メソッドに `indexPath` を渡して Item Identifier に変換してもらうことができます。今回は、 `TodoRepository` に Todo を削除する `remoteTodo` メソッドを追加していますが、これが `Todo.ID` を引数に取るので `indexPath` から変換した id を渡しています。

ちなみに、逆に `indexPath` がほしい場面で Item Identifier しか分かっていないこともあり、その場合は `UICollectionViewDiffableDataSource` の `indexPath` メソッドに Item Identifier を渡します。このように、 `indexPath` と Item Identifier の変換は `UICollectionViewDiffableDataSource` にお願いすることになります。

`TodoRepository` に Todo を削除してもらった後ですが、 Diffable DataSource / CollectionView は表示しているデータの変更を検知したりはしてくれないので、こちらからデータに変更があったことを知らせる必要があります。 Diffable DataSource を使っているため、コードを書くときに差分更新のことを考えずに０からデータを作ってあげればよいので、単に `applySnapshot` を呼んでいます。

削除時の動作は以下のようになります。

![remove.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/c2712af8-db89-72fd-27e9-c1aacdc12a4d.gif)

## Todo を編集できるようにする

続いて、表示しているデータを編集できるようにします。今回は Todo アプリなので、 Todo を Done にすることを考えます。 Cell がタップされたときに Todo の Done 状態がトグルできるのがよさそうなのでそのように `UICollectionViewDelegate` を実装していきます。 iOS 13 以降で `UICollectionView` の DataSource は進化しましたが、 Delegate 周りにはとくに変化はないようです。

必要な差分をすべて示します。

```diff_swift
final class TodoRepository {
    // ...

+   func toggleTodo(id: Todo.ID) {
+       for i in todos.indices {
+           if todos[i].id == id {
+               todos[i].done.toggle()
+           }
+       }
    }
}

final class TodoListViewController: UIViewController {
    // ...

    private func configureCollectionView() {
        // ...

+       collectionView.delegate = self
    }

    // ...
    
    private func applySnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Todo.ID>()
        snapshot.appendSections([.main])
        snapshot.appendItems(repository.todoIDs, toSection: .main)
        
        dataSource.apply(snapshot, animatingDifferences: true)
    }

+   private func applySnapshotByReconfiguringItem(todoID: Todo.ID) {
+       var snapshot = dataSource.snapshot()
+       snapshot.reconfigureItems([todoID])
+       
+       dataSource.apply(snapshot, animatingDifferences: true)
+   }
}    

+extension TodoListViewController: UICollectionViewDelegate {
+    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        collectionView.deselectItem(at: indexPath, animated: true)
+        guard let todoID = self.dataSource.itemIdentifier(for: indexPath) else { return }
+        repository.toggleTodo(id: todoID)
+        applySnapshotByReconfiguringItem(todoID: todoID)
+    }
+}           
```

まずは CollectionView Delegate の `collectionView(_:didSelectItemAt:)` メソッドでタップを検知します。このメソッドは以前からある古いもので `indexPath` ベースなので、 DataSource に `Todo.ID` に変換してもらいます。 `TodoRepository` の `toggleTodo` メソッドを呼んでタップされた Todo の `done` プロパティを更新した後に snapshot を apply しますが、これまで使ってきた `applySnapshot` とは異なる方法で apply する `applySnapshotByReconfiguringItem` メソッドを追加しています。表示する Item Identifier ではなく Item のプロパティが変わった場合は、 `NSDiffableDataSourceSnapshot` の `reconfigureItems` メソッドにプロパティが変わった Item の Identifier を渡す必要があるためです。手順は、

- `UICollectionViewDataSource` の `snapshot` メソッドを呼んで現在の `NSDiffableDataSourceSnapshot` を取得する
- 取得した `NSDiffableDataSourceSnapshot` の `reconfigureItems` にプロパティが変更された Item の Identifier を渡す 
- `NSDiffableDataSourceSnapshot` を `UICollectionViewDataSource` に `apply` する

となります。注意点として、 `reconfigureItems` は iOS 15 で追加されたメソッドなので iOS 14 以前では `reloadItems` メソッドを使う必要があります。これらの違いとしては、 `reloadItems` は Cell を dequeue し直すのに対して `reconfigureItems` は既存の Cell をそのまま使うのでよりパフォーマンスがよいという点が挙げられます。ただし、更新時に異なる Cell を使いたい場合などは `reloadItems` を使って Cell を dequeue し直す必要があります。より詳しくは `reconfigureItems` のドキュメントを参照ください。

https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot/3804468-reconfigureitems

タップ時の動作は以下のようになります。

![toggle.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/71f349e9-f11c-83f5-3aa5-32bd03daa975.gif)

## セクションヘッダを追加する

Section に対してヘッダを追加してみます。各 Item を Cell で表示しているのに対して、ヘッダは SupplementaryView を使って表示します。 Diffable DataSource においては Cell と SupplementaryView は同じような手順で追加できます。ヘッダを追加するための差分を以下に示します。

```diff_swift

final class TodoListViewController: UIViewController {
    // ...

    private func configureCollectionView() {
        let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
            var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
            configuration.trailingSwipeActionsConfigurationProvider = {
                // ...
            }
+           configuration.headerMode = .supplementary
            return NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
        }
        
        // ...
    }

    private func configureDataSource() {
        // ...
        
        dataSource = UICollectionViewDiffableDataSource(
            // ...
        )
        
+       let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { view, elementKind, indexPath in
+           var configuration = view.defaultContentConfiguration()
+           configuration.text = "Todos"
+           view.contentConfiguration = configuration
+       }
+       
+       dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
+           return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
+       }
    }
}
```

まず、ヘッダを表示するということを `UICollectionViewCompositionalLayout` に伝える必要があるので、 `UICollectionLayoutListConfiguration` の `headerMode` を `.supplementary` に設定します。デフォルトは `.none` で、ヘッダを表示しないということを意味します。今回は Section が１つしかないですが、複数 Section があって Section によってヘッダを出したり出さなかったりしたい場合は `indexPath` から Section を取得して `headerMode` の設定を分岐させることも可能です。

DataSource 側でやることとして、 `UICollectionView.SupplementaryRegistration<Supplementary>` を生成し、それをもとに `UICollectionViewDiffableDataSource` の `supplementaryViewProvider` を設定しています。この流れば Item を表示する Cell に対して `CellRegistration` と `cellProvider` を設定したのとまったく同じになっています。

`SupplementaryRegistration` の型パラメータには `UICollectionReusableView` のサブクラスを指定しますが、今回は Cell と同じ `UICollectionViewListCell` を使っています。 Header に Cell と名のつく View を使うのはやや変な感じがしますが、 `UICollectionViewListCell` はこのように Cell 以外に対しても使うことができます。これにより `SupplementaryRegistration` に渡すクロージャの第一引数の `view` は `UICollectionViewListCell` になるので、 Cell と同じように `defaultContentConfiguration` を使うことでそれらしき見た目にしてくれます。ここでは使っていませんが、 `accessories` も設定できて便利です。

`supplementaryViewProvider` では、 `SupplementaryRegistration` を使って `dequeueConfiguredReusableSupplementary` を呼ぶだけです。この流れは Cell の追加と同様です。

以上でヘッダが表示されます。

![header.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/5106f361-62cd-9268-5f53-4652c75789ff.gif)

今回はヘッダのみ追加しましたが、同じような考え方でフッタも追加することができます。

## Done になっていない　Todo　のみ表示できるようにする

最後に、Done になっていない Todo のみの表示と全ての Todo の表示を切り替えられるようにします。やることは Item Identifier を Done になっていない Todo のもののみに絞ってから snapshot を apply するだけなのでとくに新しい要素はないのですが、 Diffable DataSource のアニメーションを確認する意味でもやってみます。

```diff_swift
final class TodoRepository {
    private var todos: [Todo] = (1...30).map { i in
        Todo(id: UUID(), title: "Todo #\(i)", done: false)
    }

+   var showsOnlyUndoneTodos: Bool = false
    

-   var todoIDs: [Todo.ID] { todos.map(\.id) }
+   var todoIDs: [Todo.ID] { todos.filter { showsOnlyUndoneTodos ? !$0.done : true }.map(\.id) }
    
    // ...
    
+   func toggleVisibility() {
+       showsOnlyUndoneTodos.toggle()
+   }
}

final class TodoListViewController: UIViewController {
    // ...

    override func viewDidLoad() {
        // ...
        
+       configureNavigationBar()
        
        // ...
    }

+   private func configureNavigationBar() {
+       navigationItem.leftBarButtonItem = UIBarButtonItem(
+           image: UIImage(systemName: "line.3.horizontal.decrease"),
+           primaryAction: UIAction { [weak self] _ in
+               guard let self = self else { return }
+               self.repository.toggleVisibility()
+               self.applySnapshot()
+           }
+       )
+   }
```

これまで `TodoRepository` の `todoIDs` はすべての Todo の ID を返していましたが、今回追加した `showsOnlyUndoneTodos` プロパティが `true` のときは Done になっていない Todo の id のみを返すようにします。

何らかの方法で `showsOnlyUndoneTodos` を変更できるようにしたいので、 NavigationBar にボタンを追加して、タップ時に値をトグルするようにします。これによって `todoIDs` が変化するので、その後に `applySnapshot` を呼んで Snapshot を作り直すことで表示が変化します。 これだけで Diffable DataSource が差分を計算し、アニメーションをつけてくれます。

![filter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/5a3f4f93-53bd-4b33-35bc-15489c60ce7b.gif)

## まとめ

- CollectionView / DataSource を特徴付ける型として Section と Item があり、 Item は自身を特定できる型である Item Identifier を持つ
- DataSource におけるデータ反映の流れを以下のように作る
    - `CellRegistration` に Item を Cell の表示に反映する処理を書く
    - `UICollectionViewDiffableDataSource` の `cellProvider` に Item Identifier が渡されてくるので、そこから Item を取得し、 その Item と `CellRegistration` を使って Cell を dequeue する処理を書く
- 実際のデータ反映をするために `NSDiffableDataSourceSnapshot` を生成し、 Section と Item Identifier を追加して `UICollectionViewDiffableDataSource` に `apply` する
  - データを更新する際にも、初回の表示と同じように `NSDiffableDataSourceSnapshot` を新しいデータを使って組み立てて `apply` すれば OK
- Item Identifier ではなく特定の Item のプロパティの更新を表示に反映したい場合は `UICollectionViewDiffableDataSource` から現在の `NSDiffableDataSourceSnapshot` を取得し、 `reconfigureItems` メソッドに更新したい Item の Identifier を渡して呼んでから `apply` する
- ヘッダやフッタの追加も Cell と同じ流れで行う。 `SupplementaryRegistration` を生成し、それを `UICollectionViewDiffableDataSource` の `supplementaryViewProvider` の中で使う

## 参考

- [Advances in diffable data sources - WWDC 20](https://developer.apple.com/videos/play/wwdc2020/10045/)
- [Lists in UICollectionView - WWDC 20](https://developer.apple.com/videos/play/wwdc2020/10026/)
- [Make blazing fast lists and collection views - WWDC 21](https://developer.apple.com/videos/play/wwdc2021/10252/)
- [Updating Collection Views Using Diffable Data Sources - Apple Sample Code](https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/updating_collection_views_using_diffable_data_sources)
- [Implementing Modern Collection Views - Apple Sample Code](https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views)
- [How to add custom swipe actions to a UICollectionViewListCell? - Donny Wals](https://www.donnywals.com/how-to-add-custom-swipe-actions-to-a-uicollectionviewlistcell/)
- [Table and Collection View Cells Reload Improvements in iOS 15 - SWIFT SENPAI](https://swiftsenpai.com/development/cells-reload-improvements-ios-15/)