📚

【Swift】UICollectionViewDiffableDataSourceとNSDiffableDataSourceSnapshot

2021/10/03に公開

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

サンプルコードはこちら

どちらも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?

使い方

詳細は サンプルコード を見てください。

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

使い方

詳細は サンプルコード を見てください。

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

Discussion