An item's title changed but its cell still shows the old text after applying a new snapshot. Why, and what is the difference between reconfigureItems and reloadItems?
Predict first
What do a same-id snapshot, reconfigure and reload each trigger?
import UIKit
struct Item: Hashable, Sendable {
let id: Int
var title: String
static func == (l: Item, r: Item) -> Bool { l.id == r.id }
func hash(into h: inout Hasher) { h.combine(id) }
}
var events: [String] = []
final class Cell: UICollectionViewListCell {
override func prepareForReuse() { super.prepareForReuse(); events.append("prepareForReuse") }
}
let layout = UICollectionViewCompositionalLayout.list(using: UICollectionLayoutListConfiguration(appearance: .plain))
let cv = UICollectionView(frame: CGRect(x: 0, y: 0, width: 320, height: 600), collectionViewLayout: layout)
let reg = UICollectionView.CellRegistration<Cell, Item> { cell, _, item in
events.append("configure \(item.title)")
var c = cell.defaultContentConfiguration()
c.text = item.title
cell.contentConfiguration = c
}
let ds = UICollectionViewDiffableDataSource<Int, Item>(collectionView: cv) { cv, ip, item in
events.append("provider \(item.id)")
return cv.dequeueConfiguredReusableCell(using: reg, for: ip, item: item)
}
let w = UIWindow(frame: cv.frame)
w.addSubview(cv)
w.makeKeyAndVisible()
var s = NSDiffableDataSourceSnapshot<Int, Item>()
s.appendSections([0])
s.appendItems([Item(id: 1, title: "a"), Item(id: 2, title: "b")])
ds.apply(s, animatingDifferences: false)
cv.layoutIfNeeded()
events = []
var s2 = NSDiffableDataSourceSnapshot<Int, Item>()
s2.appendSections([0])
s2.appendItems([Item(id: 1, title: "NEW"), Item(id: 2, title: "b")])
ds.apply(s2, animatingDifferences: false)
cv.layoutIfNeeded()
print("same id, new title:", events, ds.itemIdentifier(for: IndexPath(item: 0, section: 0))!.title)
events = []
var r = ds.snapshot()
r.reconfigureItems([Item(id: 2, title: "")])
ds.apply(r, animatingDifferences: false)
cv.layoutIfNeeded()
print("reconfigure:", events)
events = []
var l = ds.snapshot()
l.reloadItems([Item(id: 2, title: "")])
ds.apply(l, animatingDifferences: false)
cv.layoutIfNeeded()
print("reload:", events)
- same id, new title: [] NEW / reconfigure: [provider 2, configure b] / reload: [provider 2, prepareForReuse, configure b]
- same id, new title: [configure NEW] / reconfigure and reload identical
- same id, new title: [] a / …
Mental model. Identity is a name tag: if the tag is unchanged the usher assumes the same guest, so a new outfit goes unnoticed until you ask the usher to take another look (reconfigure) or to seat them again from scratch (reload).
A diffable data source compares snapshots by identity (Hashable equality), not by content. When the item's == and hash use only its id, a new snapshot containing the same id with a new title is "no change": the check applied one and the cell provider ran zero times, while itemIdentifier(for:) already returned the new title, so the model and the visible cell disagree. This is Q247's identity rule showing up as a stale UI. Two ways to fix it: put a version or timestamp in the identity (then the edit looks like delete plus insert, with animation and state loss), or keep identity stable and tell the snapshot which items to refresh.
The refresh calls differ in cost. reconfigureItems (iOS 15) asks the provider and the registration for a new configuration and applies it to the existing cell; in the check the log was provider, configure, with no prepareForReuse. reloadItems dequeues a cell as if it were new, so the log also contained prepareForReuse before configure, and the cell may be a different instance with its transient state (an in-progress swipe, a focused text field) reset; it also changes the item's size lookup. Selection survived the reload in this check. Prefer reconfigureItems unless the cell type changes; the cell provider must read the latest data by identifier from your model rather than trusting the item value held by the snapshot, because the snapshot stores whichever value it received first. Apply snapshots from one queue and keep the work in the registration light.
Equal ids trigger nothing; reconfigure skips prepareForReuse and reload includes it.
Common traps
- Mutable fields in
==andhash. It makes edits look like delete and insert; the opposite mistake (identity only) leaves cells stale. - Trusting the snapshot's item value in the provider. Look the live model up by id.
They might follow up with
- Does
reconfigureItemsanimate? The configuration change itself can animate through the content configuration; the layout may self-size again. - When is
reloadItemsright? When the cell class or its heavy state must be rebuilt.