🔥
iOS14からの新しいCollectionView UICollectionViewDiffableDataSource について
DiffableDataSourceとは
iOS13から追加されたUICollectionViewDatasourceの代わりとなる新しいDataSourceです。
詳細は以下の公式ドキュメントから見ることができます。
UICollectionViewDiffableDataSource
NSCollectionViewDiffableDataSourceReference
メリット
-
DataSourceがスッキリする
-
CompositionalLayoutと合わせて使うことでレイアウト周りがシンプルで可読性高くなる
- アニメーションやページングが実装しやすくなる
- SectionやGroupを組み合わせてレイアウトのカスタマイズが可能になる
-
-
すべてがapplyメソッドで更新するように統一されている
-
apply
を使うことでDataSourceとUIの表示状態に不整合が生じてクラッシュすることがなくなる
- 複数の更新手段がなくわかりやすい
-
-
差分更新が簡単にできる
- Snapshotを更新するだけで簡単に差分更新を行うことができる
-
Snapshotの状態を取得したり設定をするための便利なメソッド達
https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot
-
- Snapshotを更新するだけで簡単に差分更新を行うことができる
デメリット
⚠️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
...
}
...
}()
}
使い方
Item
とSection
を用意する
Hashableに準拠した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の更新をしてくれる
- 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