← All topics/UIKit & SwiftUI internals

Practical interview questions

Scenario-style prompts with sample answer outlines. Focus is on how you would design and reason in real codebases.

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 prepareForReuse help?
  • 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 prepareForReuse or identity checks after await.
configure + prepareForReuse — async/await, cancel, reuse checks
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; pair willDisplay / didEndDisplaying with identity checks after async.
  • Follow-up (diffable): UICollectionViewDiffableDataSource + snapshots for animated inserts/deletes/reloads with stable Hashable IDs—fewer performBatchUpdates mistakes than manual diffing.
  • SwiftUI ForEach — easy rule: Same real-world row → same id every time. If the id keeps changing (index when the list reorders, or a new UUID() each body), SwiftUI thinks it’s a new row—rebuilds it and loses row @State. Use Identifiable with a fixed id from your domain (server id, etc.).