Question 5
Table & collection views — reuse, prefetching, and diffing
Why do images or text ‘flash’ wrong in cells during fast scrolling?
Follow-ups
- How does
prepareForReusehelp? - When would you use
UICollectionViewDiffableDataSource?
Answer outline
Cells are reused: when a cell scrolls off screen, it may be dequeued for another index path. If you don’t reset async-loaded images, spinners, or selection state, stale content appears until the new load finishes.
prepareForReuse() is where you cancel image loads (task id / URL check), clear thumbnails, reset accessibility labels, and remove gesture targets if dynamic. Pair with willDisplay for prefetch and didEndDisplaying to cancel work.
Diffable data source (UICollectionViewDiffableDataSource) applies snapshot updates with automatic animations and fewer manual performBatchUpdates bugs. Use when your model has stable identifiers; map sections/items with NSDiffableDataSourceSnapshot.
Principles
- Never trust indexPath after async work without re-validating against current data.
- Prefetching reduces perceived latency; cancel in
prepareForReuseor identity checks after await.
final class PhotoCell: UICollectionViewCell {
private var loadTask: Task<Void, Never>?
private var boundItemId: String?
func configure(with item: Item) {
loadTask?.cancel() // new configure → drop in-flight work
// assign to local state
boundItemId = item.id
imageView.image = placeholder
// hold reference to the item id passed into the function
let itemId = item.id
loadTask = Task { @MainActor [weak self] in
guard let self else { return }
do {
try Task.checkCancellation()
let (data, _) = try await URLSession.shared.data(from: item.thumbURL)
try Task.checkCancellation() // before expensive decode / UI updates
let image = UIImage(data: data)
guard !Task.isCancelled else { return }
// check the id AFTER the async
guard self.boundItemId == itemId else { return } // reused → wrong row
self.imageView.image = image ?? placeholder
} catch is CancellationError {
return
} catch {
guard !Task.isCancelled, self.boundItemId == itemId else { return }
self.imageView.image = placeholder
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
// RESET STATE
loadTask?.cancel()
loadTask = nil
boundItemId = nil
imageView.image = nil
}
}
Follow-up angles
- Follow-up (
prepareForReuse): reset UI (images, text, selection), cancel in-flight loads, clear accessibility; pairwillDisplay/didEndDisplayingwith identity checks after async. - Follow-up (diffable):
UICollectionViewDiffableDataSource+ snapshots for animated inserts/deletes/reloads with stableHashableIDs—fewerperformBatchUpdatesmistakes than manual diffing. - SwiftUI
ForEach— easy rule: Same real-world row → sameidevery time. If theidkeeps changing (index when the list reorders, or a newUUID()eachbody), SwiftUI thinks it’s a new row—rebuilds it and loses row@State. UseIdentifiablewith a fixed id from your domain (server id, etc.).