What is a diffable data source and why use it?
TL;DR
With the old data source you call insertRows, deleteRows and reloadData yourself. One wrong index and the app crashes.
A diffable data source works with snapshots. You describe the full new state. UIKit finds the differences and animates them.
Items are identified by Hashable identifiers.
The basic setup
enum Section { case main }
final class ContactsViewController: UIViewController {
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Contact.ID>!
private var contactsByID: [Contact.ID: Contact] = [:]
private func configureDataSource() {
let registration = UICollectionView.CellRegistration<UICollectionViewListCell, Contact.ID> {
[weak self] cell, _, id in
guard let contact = self?.contactsByID[id] else { return }
var content = cell.defaultContentConfiguration()
content.text = contact.name
cell.contentConfiguration = content
}
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
collectionView, indexPath, id in
collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: id)
}
}
func show(_ contacts: [Contact]) {
contactsByID = Dictionary(uniqueKeysWithValues: contacts.map { ($0.id, $0) })
var snapshot = NSDiffableDataSourceSnapshot<Section, Contact.ID>()
snapshot.appendSections([.main])
snapshot.appendItems(contacts.map(\.id))
dataSource.apply(snapshot, animatingDifferences: true)
}
}Use IDs, not full models
Apple recommends identifiers as items, not whole structs. If you use the full struct, any changed property makes a "new" item. UIKit then deletes and inserts it instead of updating it. You lose the cell state and get odd animations.
With IDs, you tell UIKit which items changed:
var snapshot = dataSource.snapshot()
snapshot.reconfigureItems([changedID]) // iOS 15+
dataSource.apply(snapshot)| Method | What it does |
|---|---|
reconfigureItems | Updates the existing cell. Fast. Keeps the cell. |
reloadItems | Throws the cell away and dequeues a new one. Slower. |
Prefer reconfigureItems for content changes.
Rules that cause crashes
- Identifiers must be unique in the whole snapshot. Duplicates crash or log errors.
- Do not mix
performBatchUpdatesorreloadDatawith a diffable data source. Use snapshots only. applycan run on a background queue, but always call it from the same queue. The main queue is the simple choice.
Section snapshots
For expandable lists (outline style), use NSDiffableDataSourceSectionSnapshot. It supports parent and child items.
var section = NSDiffableDataSourceSectionSnapshot<Contact.ID>()
section.append([groupID])
section.append(childIDs, to: groupID)
dataSource.apply(section, to: .main)Pair it with compositional layout
UICollectionViewCompositionalLayout (iOS 13) builds layouts from items, groups and sections.
For simple lists, use the list configuration:
let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
let layout = UICollectionViewCompositionalLayout.list(using: config)Why interviewers like this topic
It shows you know the classic crash: "Invalid update: invalid number of items in section". A diffable data source removes this whole class of bugs, because there is one source of truth: the snapshot.
Common follow-up questions
- Does it work with UITableView? Yes,
UITableViewDiffableDataSourcehas the same API. - How fast is the diff? It is linear time, O(n). Fine for thousands of items.
- How do you animate without diffing?
applySnapshotUsingReloadDatareplaces everything with no animation. - How do you support swipe to delete or reordering? Subclass the data source, or use
reorderingHandlersand list cell swipe actions. - What is the SwiftUI version?
ListandForEachwithIdentifiabledata do the diffing for you.