Browse Questions
  • A diffable data source is a data source that you drive with immutable snapshots of Hashable identifiers instead of index-path callbacks, and it computes the inserts, deletes, and moves for you.
  • Encapsulation, abstraction, inheritance, and polymorphism.
  • layoutSubviews is called by the system whenever a view's bounds change or its subview layout needs to be recalculated.
  • @ViewBuilder is a result builder that lets you write multiple views inside a closure and have them composed into a single view.

Answer: A diffable data source is a data source that you drive with immutable snapshots of Hashable identifiers instead of index-path callbacks, and it computes the inserts, deletes, and moves for you. It eliminates the entire class of "invalid number of rows" crashes caused by the model and the performBatchUpdates calls falling out of sync.

Code Example:

enum Section: Hashable { case pinned, all }

struct Contact: Hashable, Identifiable {
    let id: UUID
    let name: String
    let isPinned: Bool
}

final class ContactsViewController: UIViewController {

    private var dataSource: UICollectionViewDiffableDataSource<Section, Contact.ID>!
    private var contactsByID: [Contact.ID: Contact] = [:]

    private func makeDataSource() {
        let cellReg = UICollectionView.CellRegistration<UICollectionViewListCell, Contact.ID> {
            [weak self] cell, _, id in
            var config = cell.defaultContentConfiguration()
            config.text = self?.contactsByID[id]?.name
            cell.contentConfiguration = config
        }

        dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
            collectionView, indexPath, id in
            collectionView.dequeueConfiguredReusableCell(using: cellReg, for: indexPath, item: id)
        }
    }

    private func apply(_ contacts: [Contact], animated: Bool = true) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Contact.ID>()
        snapshot.appendSections([.pinned, .all])
        snapshot.appendItems(contacts.filter(\.isPinned).map(\.id), toSection: .pinned)
        snapshot.appendItems(contacts.map(\.id), toSection: .all)
        dataSource.apply(snapshot, animatingDifferences: animated)  // diffs + animates for you
    }
}

// Compositional layout — list appearance with a header
let layout = UICollectionViewCompositionalLayout { _, environment in
    var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
    config.headerMode = .supplementary
    return .list(using: config, layoutEnvironment: environment)
}

Key Points:

  • Section and item types must be Hashable, and each item's hash must be stable and unique — prefer an ID over the whole model, otherwise editing a field reads as delete + insert
  • apply(_:animatingDifferences:) is safe to call from a background queue as long as you're consistent; on iOS 15+ use await dataSource.apply(snapshot)
  • Use reconfigureItems (iOS 15+) instead of reloadItems to update a visible cell in place without recreating it
  • Pairs naturally with UICollectionViewCompositionalLayout and CellRegistration, which remove the register/reuseIdentifier string dance