🔥

iOS14からの新しいCollectionView UICollectionViewDiffableDataSource について

2022/07/03に公開

DiffableDataSourceとは

iOS13から追加されたUICollectionViewDatasourceの代わりとなる新しいDataSourceです。
詳細は以下の公式ドキュメントから見ることができます。

UICollectionViewDiffableDataSource
NSCollectionViewDiffableDataSourceReference

メリット

  • DataSourceがスッキリする

  • すべてがapplyメソッドで更新するように統一されている

    • applyを使うことでDataSourceとUIの表示状態に不整合が生じてクラッシュすることがなくなる
    • 複数の更新手段がなくわかりやすい
  • 差分更新が簡単にできる

デメリット

⚠️canEditRowAt、commit:forRowAt、canMoveRowAt、moveRowAtを使う場合はサブクラスを作成してそこに定義する必要がある

  • 個別のcellに対して編集モードを無効にしたい場合
  • cellを並び替えるイベントを検知して更新したい場合

など

final class DataSource: UITableViewDiffableDataSource<Section, Item> {
   override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
       return true
   }
}

final class SampleTableViewController: UITableViewController {
   private lazy var dataSource: DataSource = {
       let dataSource = DataSource(tableView: tableView) { (tableView, indexPath, item) -> UITableViewCell? in
           ...
       }
       ...
   }()
}

使い方

Hashableに準拠したItemSectionを用意する

struct Item: CaseIterable {
   var item1
	 var item2
   // 省略
}

enum Section: Equatable, Hashable {
   case list
   // 省略
}
  • 指定するのはSectionIdentifierTypeとItemIdentifierTypeでどちらもHashableに適合する必要がある
  • IndexPathに変わり、SectionとItemにuniqueなIdentifierを定義して使用する

Snapshotを定義する

private func setupDataSource(models: [SectionModel]) {
    var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
    snapshot.appendSections(Section.allCases)

    models.forEach {
        snapshot.appendItems($0.items, toSection: $0.section)
    }
    dataSource.apply(snapshot, animatingDifferences: false)
}

NSDiffableDataSourceSnapshot

  • collectionViewのデータの現在の状態を表す表現を返す(UIの状態を表す唯一の存在)
    • IndexPathの代わりに SectionとItemで一意の識別子(Identifier)を使用する
  • 変更がある場合はapplyメソッドで更新する
    • UIの更新時にperformBatchUpdatesではなくapplyというメソッドを使うことで、DataSourceとUIの表示状態に不整合が生じないようにUIの更新をしてくれる

💡 applyする際にanimatingDifferencesをtrueにすることで一行でアニメーションを設定することができるdataSource.apply(snapshot, animatingDifferences: true

// NSDiffableDataSourceSectionSnapshot

public struct NSDiffableDataSourceSectionSnapshot<ItemIdentifierType> where ItemIdentifierType : Hashable {

    public init()

    public init(_ snapshot: NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>)

    public mutating func append(_ items: [ItemIdentifierType], to parent: ItemIdentifierType? = nil)

    public mutating func insert(_ items: [ItemIdentifierType], before item: ItemIdentifierType)

    public mutating func insert(_ items: [ItemIdentifierType], after item: ItemIdentifierType)

    public mutating func delete(_ items: [ItemIdentifierType])

    public mutating func deleteAll()

    public mutating func expand(_ items: [ItemIdentifierType])

    public mutating func collapse(_ items: [ItemIdentifierType])

    public mutating func replace(childrenOf parent: ItemIdentifierType, using snapshot: NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>)

    public mutating func insert(_ snapshot: NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>, before item: (ItemIdentifierType))

    public mutating func insert(_ snapshot: NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>, after item: (ItemIdentifierType))

    public func isExpanded(_ item: ItemIdentifierType) -> Bool

    public func isVisible(_ item: ItemIdentifierType) -> Bool

    public func contains(_ item: ItemIdentifierType) -> Bool

    public func level(of item: ItemIdentifierType) -> Int

    public func index(of item: ItemIdentifierType) -> Int?

    public func parent(of child: ItemIdentifierType) -> ItemIdentifierType?

    public func snapshot(of parent: ItemIdentifierType, includingParent: Bool = false) -> NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>

    public var items: [ItemIdentifierType] { get }

    public var rootItems: [ItemIdentifierType] { get }

    public var visibleItems: [ItemIdentifierType] { get }
}

Advances in diffable data sources - WWDC20 - Videos - Apple Developer

Advances in diffable data sources

cellProviderで各Cellを生成する

private lazy var cellProvider: DataSource.CellProvider = { [weak self] collectionView, indexPath, item -> UICollectionViewCell? in
    guard let self = self else { return nil }
    switch row {
    case .item1:
        let cell = collectionView.dequeueReusableCell(type: Item1CollectionViewCell.self, for: indexPath)
        return cell

    case .item2:
        let cell = collectionView.dequeueReusableCell(type: Item2CollectionViewCell.self, for: indexPath)
        return cell
    }
}
  • DataSourceと関連付けたいcollectionViewにcellProviderを渡す
  • cellForItem(at:)に代わりcellProviderを使う

DataSourceにcollectionViewとcellProviderをセットする

private lazy var dataSource: DataSource = {
    .init(collectionView: collectionView, cellProvider: cellProvider)
}()

Discussion