How does a diffable data source work, and what does it require of your item identifiers?
Predict first
What does a snapshot hold after these edits?
import UIKit
var snap = NSDiffableDataSourceSnapshot<Int, Int>()
snap.appendSections([0])
snap.appendItems([1, 2, 3])
snap.deleteItems([2])
print(snap.itemIdentifiers)
- [1, 2, 3]
- [1, 3]
- [2]
Predict first
What happens when the same item identifier appears twice?
import UIKit
let cv = UICollectionView(frame: CGRect(x: 0, y: 0, width: 400, height: 400), collectionViewLayout: UICollectionViewFlowLayout())
let reg = UICollectionView.CellRegistration<UICollectionViewCell, Int> { _, _, _ in }
let ds = UICollectionViewDiffableDataSource<Int, Int>(collectionView: cv) { cv, ip, id in
cv.dequeueConfiguredReusableCell(using: reg, for: ip, item: id)
}
var snap = NSDiffableDataSourceSnapshot<Int, Int>()
snap.appendSections([0])
snap.appendItems([1, 2, 2])
ds.apply(snap, animatingDifferences: false)
print("survived")
- The duplicate is ignored
- The app crashes: identifiers must be unique
Mental model. You hand the data source a new guest list. It compares it with the previous one and works out who arrived, left, or moved seats, instead of you calling people one by one.
A UICollectionViewDiffableDataSource<Section, Item> replaces the old “answer numberOfItems and hope” protocol. You build an immutable snapshot (NSDiffableDataSourceSnapshot) listing sections and the identifiers of the items in each, and call apply(_:animatingDifferences:). The data source compares it with the current snapshot and works out the inserts, deletes and moves itself, so there are no batch-update index mismatches and no reloadData flicker. The cell provider closure receives an identifier (and index path) and returns a configured cell.
Everything rests on identity. Section and item types must be Hashable, and the hash and equality are the item’s identity, not its content. If equality includes mutable fields, editing a field looks to the data source like a deleted item and a new one and you get a visual delete and insert. Use a stable id (a UUID or database key) for identity, and look the live model up from it. To refresh a cell whose content changed without changing identity, call snapshot.reconfigureItems(_:) (cheap) rather than reloadItems. Identifiers must be unique: a duplicate identifier is a hard crash, not a silent overwrite. Apply snapshots consistently from one queue, and prefer building a fresh snapshot from your model each time over patching the old one.
A snapshot is a value you edit freely before applying it. Deleting an identifier removes it from the list.
Identity must be unique. A duplicate triggers an exception saying the supplied item identifiers are not unique.
Common traps
- Making identity depend on mutable content. An edit becomes a delete plus an insert, and the cell flickers and loses state.
- Appending the same identifier twice. It crashes with “supplied item identifiers are not unique”.
They might follow up with
- When do you use
reconfigureItems? When an item’s content changed but its identity did not, so the existing cell is updated in place. - Can you apply from a background thread? Yes, as long as you always apply from the same queue.
Related questions
- 7.5.1 How does compositional layout describe a collection view, and how do you read its output?
- 7.5.3 How does cell reuse work, and what are the classic reuse bugs?
- 7.6.1 How does a UITableView get its rows, and why do batch updates crash when the data and the update disagree?
← How does compositional layout describe a collection view, and how do… How does cell reuse work, and what are the classic reuse bugs? →