【Swift】UICollectionViewDiffableDataSourceとNSDiffableDataSourceSnapshot

#記事を移動しました。
https://zenn.dev/dd_sho/articles/73393668e7c8e7
今後は上記の記事で更新します。（2021/10/03）

iOS 13以降で使える `UICollectionViewDiffableDataSource` と `NSDiffableDataSourceSnapshot` について調べたので備忘録としてまとめます。

[サンプルコードはこちら](https://github.com/sEmoto0808/swf-diffable-data-source)

どちらもWWDC2019で `Compositional Layout` と合わせて発表されましたね。
個人的な印象では、

- データの更新に強くなった
- `Compositional Layout` と組み合わせて複雑なレイアウトの表示とデータ管理が楽になった

こんな感じの印象です。

# NSDiffableDataSourceSnapshot
## 概要
`NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>`
データを格納して管理するクラス

**SectionIdentifierType**: Sectionを定義する型でHashableであること（Enumとかが多そう）
**ItemIdentifierType**: cellに表示するデータ型でHashableであること

## 用意されているメソッド、プロパティ
### データ管理系
- データの追加・削除（SectionとItem）
 - append, insert, delete
- 並び替え（SectionとItem）
 - moveSection, moveItem
- リロード（SectionとItem）
 - reloadItems, reloadSections

### データ参照系
- データ数の取得（SectionとItem）
 - public var numberOfItems: Int { get }
 - public var numberOfSections: Int { get }
 - public func numberOfItems(inSection identifier: SectionIdentifierType) -> Int
- 値（一覧）の取得（SectionとItem）
 - public var sectionIdentifiers: [SectionIdentifierType] { get }
 - public var itemIdentifiers: [ItemIdentifierType] { get }
 - public func itemIdentifiers(inSection identifier: SectionIdentifierType) -> [ItemIdentifierType]
 - public func sectionIdentifier(containingItem identifier: ItemIdentifierType) -> SectionIdentifierType?
- indexの取得（SectionとItem）
 - public func indexOfItem(_ identifier: ItemIdentifierType) -> Int?
 - public func indexOfSection(_ identifier: SectionIdentifierType) -> Int?

## 使い方
詳細は [サンプルコード](https://github.com/sEmoto0808/swf-diffable-data-source) を見てください。

```swift:AdvancedLayoutViewController.swift

private enum Section {
    case main
}

struct SampleItemModel: Codable, Hashable {
    let value: Int
}

// initial data
let list = Array(0..<100).map { SampleItemModel(value: $0) }
var snapshot = NSDiffableDataSourceSnapshot<Section, SampleItemModel>()
snapshot.appendSections([.main])
snapshot.appendItems(list)
```

# UICollectionViewDiffableDataSource
## 概要
`UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>`
データをUIに紐付けて表示するクラス

**SectionIdentifierType**: Sectionを定義する型でHashableであること（Enumとかが多そう）
**ItemIdentifierType**: cellに表示するデータ型でHashableであること
## Initializer
- インスタンス生成

```
public typealias CellProvider = (UICollectionView, IndexPath, ItemIdentifierType) -> UICollectionViewCell?
public init(collectionView: UICollectionView, cellProvider: @escaping CellProvider)
```
- ヘッダーとフッター追加

```
public typealias SupplementaryViewProvider = (UICollectionView, String, IndexPath) -> UICollectionReusableView?
public var supplementaryViewProvider: SupplementaryViewProvider?
```
## 用意されているメソッド、プロパティ
### データ管理系
- データを適用する
 - open func apply(_ snapshot: NSDiffableDataSourceSnapshot, animatingDifferences: Bool = true, completion: (() -> Void)? = nil)

### データ参照系
- Snapshotの取得
 - open func snapshot() -> NSDiffableDataSourceSnapshot
- cellに表示するデータを取得
 - open func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType?
- indexPathの取得
 - open func indexPath(for itemIdentifier: ItemIdentifierType) -> IndexPath?
 
### 元のDataSourceにあって引き継がれた系
- @objc open func numberOfSections(in collectionView: UICollectionView) -> Int
- @objc open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
- @objc open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
- @objc open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
- @objc open func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool
- @objc open func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
- @objc open func indexTitles(for collectionView: UICollectionView) -> [String]?
- @objc open func collectionView(_ collectionView: UICollectionView, indexPathForIndexTitle title: String, at index: Int) -> IndexPath

## 使い方
詳細は [サンプルコード](https://github.com/sEmoto0808/swf-diffable-data-source) を見てください。

```swift:AdvancedLayoutViewController.swift

private enum Section {
    case main
}

struct SampleItemModel: Codable, Hashable {
    let value: Int
}

@IBOutlet weak var advancedCollectionView: UICollectionView! {
    didSet {
        advancedCollectionView.register(UINib(nibName: "LabelCell", bundle: nil),
                                        forCellWithReuseIdentifier: "LabelCell")
        advancedCollectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    }
}

private var dataSource: UICollectionViewDiffableDataSource<Section, SampleItemModel>! = nil

dataSource = UICollectionViewDiffableDataSource<Section, SampleItemModel>(collectionView: advancedCollectionView) {
    // CellProvider
    (collectionView: UICollectionView, indexPath: IndexPath, identifier: SampleItemModel) -> UICollectionViewCell? in
            
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LabelCell",
                                                        for: indexPath) as? LabelCell
        else { return UICollectionViewCell() }
    cell.set(text: "\(identifier.value)")
    return cell
}

dataSource.apply(snapshot, animatingDifferences: false)
```

# まとめ
使ってみてめちゃくちゃ便利だったのでiOS13以降のアプリで積極的に使っていこうと思いました。

ちなみに下記のライブラリを導入すると、
 `Compositional Layout` `UICollectionViewDiffableDataSource` `NSDiffableDataSourceSnapshot` 
がiOS12以前でも使えます。
ただiOS13の挙動と少し違う部分もあるみたいです。（データを追加取得した時とか）
https://github.com/kishikawakatsumi/IBPCollectionViewCompositionalLayout