Diffable Data Sources Are UIKit's Best-Kept Secret
No more index-path arithmetic and no more inconsistent-update crashes: describe the state, and let UIKit animate the difference.
The most common UIKit crash we saw in production was the old classic: the number of rows after an update didn’t match the number before plus the inserted minus the deleted. Diffable data sources make that crash impossible by design, because you never describe changes at all — only the state you want.
You build an NSDiffableDataSourceSnapshot of sections and item identifiers, apply it, and the data source computes the inserts, deletes and moves itself:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum Section { case main }
final class RecipesViewController: UICollectionViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Recipe.ID>!
private var recipes: [Recipe.ID: Recipe] = [:]
private func configureDataSource() {
let cell = UICollectionView.CellRegistration<UICollectionViewListCell, Recipe.ID> {
[unowned self] cell, _, id in
var content = cell.defaultContentConfiguration()
content.text = recipes[id]?.title
cell.contentConfiguration = content
}
dataSource = .init(collectionView: collectionView) { view, indexPath, id in
view.dequeueConfiguredReusableCell(using: cell, for: indexPath, item: id)
}
}
func show(_ new: [Recipe]) {
recipes = Dictionary(uniqueKeysWithValues: new.map { ($0.id, $0) })
var snapshot = NSDiffableDataSourceSnapshot<Section, Recipe.ID>()
snapshot.appendSections([.main])
snapshot.appendItems(new.map(\.id))
dataSource.apply(snapshot, animatingDifferences: true)
}
}
Identifiers, not models
Notice the item type is Recipe.ID, not Recipe. If you use the whole model as the identifier, editing a title changes its hash and the diff sees a delete plus an insert rather than an update. Store identifiers in the snapshot, look models up in the cell provider, and when content changes in place call reconfigureItems(_:) on the snapshot.
Pair this with compositional layout and list configurations and most table-view boilerplate simply disappears.
— 30 —