Diffable Data Sources in UIKit (UITableView + UICollectionView) — Modern Swift (2026)
Diffable Data Sources in UIKit (UITableView + UICollectionView) — Modern Swift (2026)
Diffable Data Sources let you drive UITableView and UICollectionView from snapshots (a declarative “this is what the UI should show right now” state) instead of manually coordinating insertRows, deleteItems, performBatchUpdates, etc. Apple’s snapshot API requires unique, Hashable identifiers for sections and items. (Apple Developer)
Key idea: your UI is a projection of a snapshot. You build a snapshot from your current model state and apply it; the system computes the updates for you. (Apple Developer)
Table of contents
- 1. Core types and mental model
- 2. Identifier design (most important “gotcha”)
- 3. Modern “latest” apply syntax:
async+@MainActor - 4. UITableView examples
- 5. UICollectionView examples
- 6. Snapshot operations cookbook
- 7. Concurrency + performance notes (Swift 6 era)
- 8. Core Data integration pattern (FRC → diffable)
- 9. Common pitfalls checklist
1. Core types and mental model
The main actors
- Data source
UITableViewDiffableDataSource<SectionID, ItemID>UICollectionViewDiffableDataSource<SectionID, ItemID>(Apple Developer)
- Snapshot
NSDiffableDataSourceSnapshot<SectionID, ItemID>: the entire UI state (Apple Developer)
- Section snapshot (optional / advanced)
NSDiffableDataSourceSectionSnapshot<ItemID>: per-section state, supports hierarchy/outline and independent section updates (Apple Developer)
The flow
- Pick identifier types (
Hashable, unique). (Apple Developer) - Create a diffable data source with a cell provider.
- Build a snapshot reflecting current model state.
- Apply snapshot to render UI updates.
2. Identifier design (most important “gotcha”)
Apple’s snapshot docs explicitly recommend Swift value types (struct/enum) for identifiers; if you use a Swift class it must be an NSObject subclass. (Apple Developer)
Rules of thumb
✅ Good identifiers
UUID,Int,Stringstruct ItemID: Hashable { let rawValue: UUID }enum Section: Hashable { case main, favorites }
❌ Risky identifiers
- A whole mutable model (
struct Contact { var name: String ... }) if itsHashableincludes mutable properties. - Anything where
hashValuecan change over the lifetime of an item.
Recommended pattern: “IDs in snapshots, models in a store”
- Snapshot contains IDs.
- Cell provider looks up the current model from a dictionary/cache by ID.
Why? Because the itemIdentifier passed into your cell provider should be treated primarily as an identifier, and your “fresh data” should come from your own source-of-truth store (a common source of confusion when reloading). (Stack Overflow)
3. Modern “latest” apply syntax: async + @MainActor
In newer SDKs, apply is available as an async method and annotated with @MainActor. (Apple Developer)
// Modern (Swift Concurrency-friendly)
await dataSource.apply(snapshot, animatingDifferences: true)
There are also completion-handler variants in the API surface (useful for compatibility or when you need an explicit callback). (Apple Developer)
Important concurrency nuance (iOS 18+ era)
UIKit docs historically said you could call apply from a background queue if done consistently, but the API gained @MainActor annotations in the iOS 18 SDK and that created real-world Swift 6 friction; Jesse Squires documents the change and UIKit’s explanation. (Jesse Squires)
Practical guidance (2026):
- Build snapshots off-thread if you want (pure data).
- Apply snapshots on the main actor (either by being in
@MainActorcontext or usingawait MainActor.run { ... }). (Apple Developer)
4. UITableView examples
Note: Apple explicitly warns you shouldn’t swap out the table view’s data source after configuring a diffable data source; if you truly need a new data source, create a new table view and data source. (Apple Developer)
Shared model helpers used by table examples
import UIKit
enum Section: Hashable {
case main
case favorites
}
struct Contact: Identifiable, Hashable {
let id: UUID
var name: String
var isFavorite: Bool
// Stable identity: only ID participates in Hashable/Equatable.
func hash(into hasher: inout Hasher) { hasher.combine(id) }
static func == (lhs: Contact, rhs: Contact) -> Bool { lhs.id == rhs.id }
}
4.1 Minimal table view
This example uses:
UITableViewDiffableDataSourceUITableView.CellRegistrationUIListContentConfiguration(modern cell content API) (Apple Developer)await dataSource.apply(...)(modern apply) (Apple Developer)
@MainActor
final class ContactsTableViewController: UIViewController {
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
// Keep a strong reference! tableView.dataSource is a weak reference in UIKit patterns.
private var dataSource: UITableViewDiffableDataSource<Section, UUID>!
// Source of truth store
private var contactsByID: [UUID: Contact] = [:]
private var orderedIDs: [UUID] = []
private lazy var cellRegistration = UITableView.CellRegistration<UITableViewCell, Contact> { cell, _, contact in
var content = cell.defaultContentConfiguration()
content.text = contact.name
content.secondaryText = contact.isFavorite ? "★ Favorite" : nil
cell.contentConfiguration = content
cell.accessoryType = .disclosureIndicator
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Contacts"
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
configureDataSource()
seedData()
Task { await applySnapshot(animated: false) }
}
private func configureDataSource() {
dataSource = UITableViewDiffableDataSource<Section, UUID>(tableView: tableView) { [weak self] tableView, indexPath, contactID in
guard let self, let contact = self.contactsByID[contactID] else { return nil }
// Note: cell registration item type can be the *model* (Contact),
// even though diffable item identifier is UUID.
return tableView.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: contact)
}
}
private func seedData() {
let contacts: [Contact] = [
.init(id: UUID(), name: "Alicia", isFavorite: true),
.init(id: UUID(), name: "Ben", isFavorite: false),
.init(id: UUID(), name: "Chen", isFavorite: false),
]
contactsByID = Dictionary(uniqueKeysWithValues: contacts.map { ($0.id, $0) })
orderedIDs = contacts.map(\.id)
}
private func makeSnapshot() -> NSDiffableDataSourceSnapshot<Section, UUID> {
var snapshot = NSDiffableDataSourceSnapshot<Section, UUID>()
snapshot.appendSections([.main])
snapshot.appendItems(orderedIDs, toSection: .main)
return snapshot
}
private func applySnapshot(animated: Bool) async {
let snapshot = makeSnapshot()
await dataSource.apply(snapshot, animatingDifferences: animated)
}
}
Why the cell registration uses Contact while diffable uses UUID: Apple notes the registration item type doesn’t have to match the diffable item identifier type. (Apple Developer)
4.2 Table with multiple sections + section headers
There are multiple approaches for headers:
- Use the table view delegate to provide custom header views.
- Or subclass
UITableViewDiffableDataSourceand overridetableView(_:titleForHeaderInSection:)(common pattern). (Stack Overflow)
Here’s the subclass approach:
final class HeaderedContactsDataSource: UITableViewDiffableDataSource<Section, UUID> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let sectionID = sectionIdentifier(for: section) else { return nil }
switch sectionID {
case .favorites: return "Favorites"
case .main: return "All Contacts"
}
}
}
Then create it:
dataSource = HeaderedContactsDataSource(tableView: tableView) { [weak self] tableView, indexPath, id in
guard let self, let contact = self.contactsByID[id] else { return nil }
return tableView.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: contact)
}
Build a snapshot that splits items:
private func makeSnapshot() -> NSDiffableDataSourceSnapshot<Section, UUID> {
var snapshot = NSDiffableDataSourceSnapshot<Section, UUID>()
let favorites = orderedIDs.filter { contactsByID[$0]?.isFavorite == true }
let others = orderedIDs.filter { contactsByID[$0]?.isFavorite != true }
snapshot.appendSections([.favorites, .main])
snapshot.appendItems(favorites, toSection: .favorites)
snapshot.appendItems(others, toSection: .main)
return snapshot
}
4.3 Swipe-to-delete + snapshot-driven deletes
For tables, it’s common to implement swipe actions in the view controller (delegate) and then update your model + apply a new snapshot.
extension ContactsTableViewController: UITableViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
}
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
guard let id = dataSource.itemIdentifier(for: indexPath) else { return nil }
let delete = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, done in
guard let self else { return }
self.contactsByID[id] = nil
self.orderedIDs.removeAll { $0 == id }
Task { await self.applySnapshot(animated: true) }
done(true)
}
return UISwipeActionsConfiguration(actions: [delete])
}
}
4.4 Updating a single row: reconfigureItems vs reloadItems
Apple’s guidance: if you want to update the contents of existing cells without replacing them, prefer reconfigureItems(_:) over reloadItems(_:) for performance, unless you truly need a full reload. (Apple Developer)
Example: toggle favorite flag for one contact.
@MainActor
func toggleFavorite(for id: UUID) async {
guard var contact = contactsByID[id] else { return }
contact.isFavorite.toggle()
contactsByID[id] = contact
var snapshot = dataSource.snapshot()
snapshot.reconfigureItems([id]) // lightweight refresh of visible/prefetched cells
await dataSource.apply(snapshot, animatingDifferences: true)
}
When to use reloadItems:
- You changed the cell type (rare / usually a smell).
- You must trigger a full reload lifecycle (
prepareForReuse, etc.). - You need the “replace cell” behavior rather than “reconfigure existing cell”.
Also note a common confusion: reloading doesn’t mean the identifier changed; you still use your own backing store to provide updated content. (Stack Overflow)
4.5 Fast “reset” updates: applySnapshotUsingReloadData
applySnapshotUsingReloadData resets UI to match the snapshot without computing a diff and without animating changes. (Apple Developer)
This is useful when:
- You have massive changes and don’t want animations.
- You’re switching entire datasets (e.g., changing accounts).
- You’re hitting edge cases where animated diffs get expensive.
@MainActor
func replaceAll(with contacts: [Contact]) async {
contactsByID = Dictionary(uniqueKeysWithValues: contacts.map { ($0.id, $0) })
orderedIDs = contacts.map(\.id)
let snapshot = makeSnapshot()
await dataSource.applySnapshotUsingReloadData(snapshot)
}
4.6 Filtering + search
A simple approach: keep a full list of IDs and a current filter, then build snapshot from the filtered IDs.
enum Filter {
case all
case favoritesOnly
case query(String)
}
private var filter: Filter = .all
private func filteredIDs() -> [UUID] {
switch filter {
case .all:
return orderedIDs
case .favoritesOnly:
return orderedIDs.filter { contactsByID[$0]?.isFavorite == true }
case .query(let q):
let lower = q.lowercased()
return orderedIDs.filter { (contactsByID[$0]?.name.lowercased().contains(lower) ?? false) }
}
}
private func makeSnapshot() -> NSDiffableDataSourceSnapshot<Section, UUID> {
var snapshot = NSDiffableDataSourceSnapshot<Section, UUID>()
snapshot.appendSections([.main])
snapshot.appendItems(filteredIDs(), toSection: .main)
return snapshot
}
Hook it up to a UISearchController and call Task { await applySnapshot(animated: true) } when the query changes.
5. UICollectionView examples
5.1 List-style collection view (modern replacement for many tables)
Using:
UICollectionLayoutListConfiguration+ compositional list layoutUICollectionView.CellRegistrationUICollectionViewDiffableDataSource
@MainActor
final class SettingsListViewController: UIViewController, UICollectionViewDelegate {
enum Section: Hashable { case main }
struct Setting: Hashable, Identifiable {
let id: UUID
var title: String
var isOn: Bool
func hash(into hasher: inout Hasher) { hasher.combine(id) }
static func ==(l: Self, r: Self) -> Bool { l.id == r.id }
}
private var settingsByID: [UUID: Setting] = [:]
private var orderedIDs: [UUID] = []
private lazy var collectionView: UICollectionView = {
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
let layout = UICollectionViewCompositionalLayout.list(using: config)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.delegate = self
return cv
}()
private var dataSource: UICollectionViewDiffableDataSource<Section, UUID>!
// IMPORTANT: create registrations outside the cell provider (Apple warns against doing so inside).
// Doing it inside prevents reuse and can throw exceptions (iOS 15+). :contentReference[oaicite:20]{index=20}
private lazy var cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Setting> { cell, _, setting in
var content = UIListContentConfiguration.cell()
content.text = setting.title
cell.contentConfiguration = content
// Use a checkmark accessory for demo
cell.accessories = setting.isOn ? [.checkmark()] : []
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Settings"
view.addSubview(collectionView)
collectionView.frame = view.bounds
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
configureDataSource()
seed()
Task { await applySnapshot(animated: false) }
}
private func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, UUID>(collectionView: collectionView) { [weak self] collectionView, indexPath, id in
guard let self, let setting = self.settingsByID[id] else { return nil }
return collectionView.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: setting)
}
}
private func seed() {
let items: [Setting] = [
.init(id: UUID(), title: "Wi‑Fi", isOn: true),
.init(id: UUID(), title: "Bluetooth", isOn: false),
.init(id: UUID(), title: "Airplane Mode", isOn: false),
]
settingsByID = Dictionary(uniqueKeysWithValues: items.map { ($0.id, $0) })
orderedIDs = items.map(\.id)
}
private func makeSnapshot() -> NSDiffableDataSourceSnapshot<Section, UUID> {
var snap = NSDiffableDataSourceSnapshot<Section, UUID>()
snap.appendSections([.main])
snap.appendItems(orderedIDs, toSection: .main)
return snap
}
private func applySnapshot(animated: Bool) async {
await dataSource.apply(makeSnapshot(), animatingDifferences: animated)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let id = dataSource.itemIdentifier(for: indexPath),
var setting = settingsByID[id] else { return }
setting.isOn.toggle()
settingsByID[id] = setting
Task {
var snap = dataSource.snapshot()
snap.reconfigureItems([id])
await dataSource.apply(snap, animatingDifferences: true)
}
}
}
5.2 Section headers with SupplementaryRegistration
UICollectionViewDiffableDataSource supports a supplementaryViewProvider closure for headers/footers. (Apple Developer) Modern pattern: UICollectionView.SupplementaryRegistration + dequeueConfiguredReusableSupplementary. (Apple Developer)
private lazy var headerRegistration =
UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(
elementKind: UICollectionView.elementKindSectionHeader
) { [weak self] header, _, indexPath in
guard let self else { return }
let sectionID = self.dataSource.sectionIdentifier(for: indexPath.section)
var content = UIListContentConfiguration.groupedHeader()
content.text = (sectionID == .main) ? "General" : "Other"
header.contentConfiguration = content
}
private func configureHeader() {
dataSource.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in
guard let self, kind == UICollectionView.elementKindSectionHeader else { return nil }
return collectionView.dequeueConfiguredReusableSupplementary(using: self.headerRegistration, for: indexPath)
}
}
5.3 Grid layout (compositional) + diffable
A classic “photo grid” example.
@MainActor
final class PhotoGridViewController: UIViewController {
enum Section: Hashable { case main }
struct Photo: Hashable, Identifiable {
let id: UUID
let title: String
func hash(into hasher: inout Hasher) { hasher.combine(id) }
static func ==(l: Self, r: Self) -> Bool { l.id == r.id }
}
private var photos: [Photo] = (0..<60).map {
Photo(id: UUID(), title: "Photo \($0)")
}
private lazy var collectionView: UICollectionView = {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 4, leading: 4, bottom: 4, trailing: 4)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(1.0/3.0))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 3)
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return UICollectionView(frame: .zero, collectionViewLayout: layout)
}()
private var dataSource: UICollectionViewDiffableDataSource<Section, UUID>!
private lazy var cellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, Photo> { cell, _, photo in
var content = UIListContentConfiguration.cell()
content.text = photo.title
cell.contentConfiguration = content
var bg = UIBackgroundConfiguration.listPlainCell()
bg.cornerRadius = 12
cell.backgroundConfiguration = bg
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Grid"
view.addSubview(collectionView)
collectionView.frame = view.bounds
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
configureDataSource()
Task { await applySnapshot(animated: false) }
}
private func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, UUID>(collectionView: collectionView) { [weak self] cv, indexPath, id in
guard let self,
let photo = self.photos.first(where: { $0.id == id }) else { return nil }
return cv.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: photo)
}
}
private func applySnapshot(animated: Bool) async {
var snap = NSDiffableDataSourceSnapshot<Section, UUID>()
snap.appendSections([.main])
snap.appendItems(photos.map(\.id), toSection: .main)
await dataSource.apply(snap, animatingDifferences: animated)
}
}
5.4 Expandable outline / hierarchy with NSDiffableDataSourceSectionSnapshot
NSDiffableDataSourceSectionSnapshot is available for hierarchical structures (outline / expandable items). (Apple Developer)
Model:
struct Node: Hashable, Identifiable {
let id: UUID
let title: String
var children: [Node] = []
func hash(into hasher: inout Hasher) { hasher.combine(id) }
static func ==(l: Self, r: Self) -> Bool { l.id == r.id }
}
Apply section snapshot to a section:
@MainActor
func applyOutline(nodes: [Node]) async {
// Flatten to lookup store
var store: [UUID: Node] = [:]
func index(_ node: Node) {
store[node.id] = node
node.children.forEach(index)
}
nodes.forEach(index)
self.nodesByID = store
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<UUID>()
func append(_ node: Node, to parent: UUID?) {
sectionSnapshot.append([node.id], to: parent)
for child in node.children {
append(child, to: node.id)
}
}
// Add roots
for root in nodes { append(root, to: nil) }
await dataSource.apply(sectionSnapshot, to: .main, animatingDifferences: true)
}
You can also hook into sectionSnapshotHandlers for expand/collapse behaviors. (Apple Developer)
5.5 Reordering with reorderingHandlers + transactions
UICollectionViewDiffableDataSource supports reordering via reorderingHandlers. (Apple Developer) The system calls your handler after a reordering transaction so you can update your backing store. (Apple Developer)
@MainActor
func enableReordering() {
dataSource.reorderingHandlers.canReorderItem = { _ in true }
dataSource.reorderingHandlers.didReorder = { [weak self] transaction in
guard let self else { return }
// transaction.finalSnapshot / transaction.difference can be used to update your store
// Example strategy: rebuild orderedIDs from finalSnapshot.
let final = transaction.finalSnapshot
self.orderedIDs = final.itemIdentifiers
}
}
6. Snapshot operations cookbook
A snapshot is just a value type representing the desired state. You add/delete/move sections and items, then apply.
Create sections + items
var snap = NSDiffableDataSourceSnapshot<Section, UUID>()
snap.appendSections([.main, .favorites])
snap.appendItems(mainIDs, toSection: .main)
snap.appendItems(favIDs, toSection: .favorites)
await dataSource.apply(snap, animatingDifferences: true)
Insert items at the top of a section
var snap = dataSource.snapshot()
snap.insertItems([newID], beforeItem: snap.itemIdentifiers(inSection: .main).first!)
await dataSource.apply(snap, animatingDifferences: true)
Delete items
var snap = dataSource.snapshot()
snap.deleteItems([idToDelete])
await dataSource.apply(snap, animatingDifferences: true)
Move an item
var snap = dataSource.snapshot()
snap.moveItem(id, afterItem: otherID)
await dataSource.apply(snap, animatingDifferences: true)
Reconfigure (lightweight refresh) vs reload
var snap = dataSource.snapshot()
snap.reconfigureItems([id]) // preferred when possible :contentReference[oaicite:27]{index=27}
await dataSource.apply(snap, animatingDifferences: true)
“No diff, no animation” reset
await dataSource.applySnapshotUsingReloadData(fullSnapshot) // :contentReference[oaicite:28]{index=28}
7. Concurrency + performance notes (Swift 6 era)
Apply is main-actor constrained
Modern SDK signatures show apply as @MainActor and async. (Apple Developer) If you were applying snapshots on background queues historically, expect Swift 6 strict concurrency to push you toward main-actor application. (Jesse Squires)
Diffing cost
Apple’s docs describe the diff as an O(n) operation where n is item count. (Apple Developer) If you have huge datasets, prefer:
- pagination / incremental loading
- stable, efficient identifiers (fast hashing/equality)
- avoid rebuilding unnecessarily-large snapshots
UIKit team guidance (as reported) suggests diffing is usually not the bottleneck compared to cell creation/layout/measurement. (Jesse Squires)
iOS 15 behavior change: animatingDifferences: false no longer equals “reloadData”
Historically, animatingDifferences: false behaved like reloadData, but as of iOS 15 diffing always happens; to explicitly reload without diffing use applySnapshotUsingReloadData. (Jesse Squires)
8. Core Data integration pattern (FRC → diffable)
NSFetchedResultsController has a delegate method that can hand you a snapshot reference; SwiftLee shows a robust pattern for converting it to a typed snapshot, optionally reloading updated items, and applying it. (SwiftLee)
A simplified outline (collection view example):
@available(iOS 13.0, *)
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
guard let dataSource = collectionView.dataSource as? UICollectionViewDiffableDataSource<Int, NSManagedObjectID> else {
return
}
var typed = snapshot as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>
// Optional: compute which IDs need reload (if object updated but ID unchanged)
// typed.reloadItems(reloadIDs)
let animate = collectionView.numberOfSections != 0
dataSource.apply(typed, animatingDifferences: animate)
}
Key takeaways from the Core Data case:
- Keep your diffable data source strongly referenced. (SwiftLee)
- When identifiers don’t change (e.g.
NSManagedObjectID), you may need explicit reload/reconfigure logic for updated objects. (SwiftLee)
9. Common pitfalls checklist
✅ Identifiers
- ☐ Section + item identifiers are unique and
Hashable. (Apple Developer) - ☐ Identifiers are stable (hash/equality won’t change as model fields change).
- ☐ Prefer value types (struct/enum) as identifiers; class identifiers must be
NSObjectsubclasses. (Apple Developer)
✅ Data source lifecycle
- ☐ You keep a strong reference to the diffable data source (don’t rely on
tableView.dataSource/collectionView.dataSource). (SwiftLee) - ☐ For table views: don’t swap out
dataSourceafter configuring a diffable data source. (Apple Developer)
✅ Cell registration
- ☐ For collection views: don’t create
UICollectionView.CellRegistrationinside the cell provider; it breaks reuse and can crash on iOS 15+. (Apple Developer)
✅ Updating items
- ☐ Use
reconfigureItemswhen you can (lighter than reload). (Apple Developer) - ☐ If you need “no diff, just reset”, use
applySnapshotUsingReloadData. (Apple Developer)
✅ Concurrency
- ☐ Apply snapshots on the main actor (
@MainActor,await MainActor.run, etc.). (Apple Developer)