Swift By Rahul

UICollectionView Diffable Data Source Explained

For years, managing data in UICollectionView (and UITableView) was a manual and often error-prone process. We'd update our underlying data model, then call reloadData() or meticulously calculate insertItems(at:), deleteItems(at:), and reloadItems(at:) to animate changes. This often led to out-of-sync data, crashes due to incorrect index paths, and UI glitches. If you've ever battled NSInternalInconsistencyException or Invalid update: invalid number of sections, you know the pain.

Enter UICollectionViewDiffableDataSource (and its table view counterpart UITableViewDiffableDataSource), introduced at WWDC 2019. This powerful API fundamentally changes how we interact with UICollectionView data, making it safer, simpler, and more robust. It leverages Apple's diffing algorithm to automatically calculate the differences between two states of your data and apply the minimal set of updates to the collection view, complete with seamless animations.

In this article, we'll dive deep into UICollectionViewDiffableDataSource, exploring its core concepts, how to set it up, and best practices for managing your collection view data with confidence.

Traditional vs Diffable Data Source for UICollectionView Traditional Data Source Data Array Manual Updates (reloadData(), insert, delete) UICollectionView Error-prone, Index Path Mismatches, Crashes Diffable Data Source Data Array Create Snapshot (apply() with diffing) UICollectionView Automatic Animations, Stable, Less Boilerplate

Understanding the Core Concepts

At the heart of UICollectionViewDiffableDataSource are two main components:

  1. UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>: This is the data source object itself. You initialize it with your collection view and a cell provider closure. It manages the communication between your data and the collection view.
    • SectionIdentifierType: A type that uniquely identifies your sections.
    • ItemIdentifierType: A type that uniquely identifies your items within sections.
    • Crucially, both SectionIdentifierType and ItemIdentifierType MUST conform to Hashable. This is what allows the diffing algorithm to efficiently compare items and sections.
  1. NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>: This is a lightweight, immutable representation of the current state of your collection view's data. You create a snapshot, populate it with your sections and items, and then apply it to the diffableDataSource. The diffable data source then compares this new snapshot with its previous state, calculates the differences, and performs the necessary updates on the UICollectionView.

Setting Up a Basic Diffable Data Source

Let's walk through setting up a simple UICollectionView that displays a list of items using DiffableDataSource.

First, define your SectionIdentifierType and ItemIdentifierType. For simplicity, we'll use enums here, but structs or even String / UUID can work as long as they are Hashable.

import UIKit

// 1. Define Section and Item Identifiers
enum Section: Hashable {
    case main
    case favorites
}

struct Fruit: Hashable {
    let name: String
    let emoji: String
    let id = UUID() // Essential for unique identification if name isn't unique

    func hash(into hasher: inout Hasher) {
        hasher.combine(id) // Only hash the unique ID
    }

    static func == (lhs: Fruit, rhs: Fruit) -> Bool {
        lhs.id == rhs.id
    }
}

class FruitsViewController: UIViewController {

    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Fruit>!

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Diffable Fruits"
        configureHierarchy()
        configureDataSource()
        applyInitialSnapshot()
    }

    // 2. Configure Collection View Layout
    private func configureHierarchy() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .systemBackground
        view.addSubview(collectionView)
    }

    private func createLayout() -> UICollectionViewLayout {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                              heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                               heightDimension: .absolute(44))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
        section.interGroupSpacing = 5

        return UICollectionViewCompositionalLayout(section: section)
    }

    // 3. Configure the Diffable Data Source
    private func configureDataSource() {
        // Cell Registration: Define how to configure a cell for an item
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Fruit> { (cell, indexPath, fruit) in
            var content = cell.defaultContentConfiguration()
            content.text = "\(fruit.emoji) \(fruit.name)"
            cell.contentConfiguration = content
            cell.accessories = [.disclosureIndicator()]
        }

        // Data Source Initialization: Connects the collection view with your data
        dataSource = UICollectionViewDiffableDataSource<Section, Fruit>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, fruit: Fruit) -> UICollectionViewCell? in
            // Dequeue and configure the cell using the registration
            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: fruit)
        }
    }

    // 4. Apply an Initial Snapshot
    private func applyInitialSnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Fruit>()
        snapshot.appendSections([.main])
        snapshot.appendItems([
            Fruit(name: "Apple", emoji: "๐ŸŽ"),
            Fruit(name: "Banana", emoji: "๐ŸŒ"),
            Fruit(name: "Orange", emoji: "๐ŸŠ"),
            Fruit(name: "Grape", emoji: "๐Ÿ‡")
        ], toSection: .main)

        dataSource.apply(snapshot, animatingDifferences: false) // 'false' for initial load, 'true' for updates
    }
}

In this setup: We define Section and Fruit types that conform to Hashable. Fruit has a UUID to ensure uniqueness, even if two fruits have the same name. configureHierarchy() sets up a basic UICollectionView with a compositional layout. configureDataSource() is where the magic happens. We use UICollectionView.CellRegistration for modern cell configuration and then initialize our UICollectionViewDiffableDataSource, providing a closure that dequeues and configures cells. applyInitialSnapshot() creates an NSDiffableDataSourceSnapshot, adds sections and items, and then applies it to the dataSource.

Working with Snapshots

The real power of DiffableDataSource comes when your data changes. Instead of manually updating the collection view, you create a new snapshot representing the desired state and apply it.

Let's add some functionality to our FruitsViewController to update the data.

// ... inside FruitsViewController class ...

private var currentFruits: [Fruit] = [
    Fruit(name: "Apple", emoji: "๐ŸŽ"),
    Fruit(name: "Banana", emoji: "๐ŸŒ"),
    Fruit(name: "Orange", emoji: "๐ŸŠ"),
    Fruit(name: "Grape", emoji: "๐Ÿ‡")
]

private var favoriteFruits: [Fruit] = []

// Modify applyInitialSnapshot to use currentFruits
override func viewDidLoad() {
    super.viewDidLoad()
    title = "Diffable Fruits"
    navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addRandomFruit))
    navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Toggle Favorites", style: .plain, target: self, action: #selector(toggleFavoriteSection))
    configureHierarchy()
    configureDataSource()
    applyCurrentStateSnapshot() // Renamed for clarity
}

// New method to apply the current state from our arrays
private func applyCurrentStateSnapshot(animatingDifferences: Bool = true) {
    var snapshot = NSDiffableDataSourceSnapshot<Section, Fruit>()

    // Add main section
    snapshot.appendSections([.main])
    snapshot.appendItems(currentFruits, toSection: .main)

    // Add favorites section if there are favorites
    if !favoriteFruits.isEmpty {
        snapshot.appendSections([.favorites])
        snapshot.appendItems(favoriteFruits, toSection: .favorites)
    }

    dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}

@objc private func addRandomFruit() {
    let newFruitNames = ["Kiwi", "Mango", "Pineapple", "Strawberry", "Cherry"]
    let newFruitEmojis = ["๐Ÿฅ", "๐Ÿฅญ", "๐Ÿ", "๐Ÿ“", "๐Ÿ’"]
    let randomIndex = Int.random(in: 0..<newFruitNames.count)
    let newFruit = Fruit(name: newFruitNames[randomIndex], emoji: newFruitEmojis[randomIndex])

    currentFruits.append(newFruit)
    applyCurrentStateSnapshot() // Apply the new snapshot with animation
}

@objc private func toggleFavoriteSection() {
    if favoriteFruits.isEmpty {
        // Move a fruit from main to favorites
        if let fruitToMove = currentFruits.first {
            currentFruits.removeFirst()
            favoriteFruits.append(fruitToMove)
        }
    } else {
        // Move a fruit from favorites back to main
        if let fruitToMove = favoriteFruits.first {
            favoriteFruits.removeFirst()
            currentFruits.append(fruitToMove)
        }
    }
    applyCurrentStateSnapshot()
}

// You might also want to add a header for sections
private func createLayout() -> UICollectionViewLayout {
    // ... (existing item and group setup) ...

    let section = NSCollectionLayoutSection(group: group)
    section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
    section.interGroupSpacing = 5

    // Section Header
    let sectionHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                   heightDimension: .estimated(44))
    let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
        layoutSize: sectionHeaderSize,
        elementKind: UICollectionView.elementKindSectionHeader,
        alignment: .top
    )
    section.boundarySupplementaryItems = [sectionHeader]

    return UICollectionViewCompositionalLayout(section: section)
}

// Configure supplementary views (like headers)
private func configureDataSource() {
    // ... (existing cell registration) ...

    dataSource = UICollectionViewDiffableDataSource<Section, Fruit>(collectionView: collectionView) {
        (collectionView: UICollectionView, indexPath: IndexPath, fruit: Fruit) -> UICollectionViewCell? in
        return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: fruit)
    }

    // Supplementary View Provider for headers
    let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) {
        (supplementaryView, elementKind, indexPath) in
        guard let section = self.dataSource.sectionIdentifier(for: indexPath.section) else { return }
        var content = supplementaryView.defaultContentConfiguration()
        content.text = (section == .main) ? "All Fruits" : "Favorite Fruits"
        supplementaryView.contentConfiguration = content
    }

    dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) -> UICollectionReusableView? in
        return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
    }
}

Now, when you tap "Add Random Fruit" or "Toggle Favorites", the applyCurrentStateSnapshot() method is called. It reconstructs the entire desired state of the collection view and asks the dataSource to update. The dataSource efficiently calculates the differences and animates the changes for you.

Here's a simple ASCII diagram illustrating the data flow:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Your Data Model  โ”‚
โ”‚ (e.g., [Fruit])   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Create New        โ”‚
โ”‚ NSDiffableDataSourceSnapshot โ”‚
โ”‚ (append sections/items)     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ dataSource.apply( โ”‚
โ”‚   newSnapshot,    โ”‚
โ”‚   animatingDifferences: true โ”‚
โ”‚ )                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Diffing Algorithm โ”‚
โ”‚    (internal)     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ UICollectionView  โ”‚
โ”‚  Updates (e.g.,   โ”‚
โ”‚  insert, delete,  โ”‚
โ”‚  reload, move)    โ”‚
โ”‚  with Animations  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Benefits and Best Practices

  1. Automatic Animations: By simply applying a new snapshot with animatingDifferences: true, you get smooth, automatic animations for inserts, deletes, reloads, and moves, without any manual calculation of index paths.
  2. State-Driven Updates: You describe what your UI should look like, not how to change it. This declarative approach makes your code easier to read, write, and maintain.
  3. Reduced Boilerplate: Gone are the days of implementing numberOfSections, numberOfItemsInSection, and cellForItemAt alongside complex update logic. The DiffableDataSource handles all of that.
  4. Eliminates NSInternalInconsistencyException: Since the diffing algorithm ensures the data source's state is always consistent with the UI, you largely avoid the notorious crashes caused by mismatched index paths.
  5. Thread Safety: While NSDiffableDataSourceSnapshot itself is immutable and can be constructed on any thread, dataSource.apply() must be called on the main thread. If you're performing heavy data processing, do it on a background queue, then dispatch the snapshot application to the main queue.
UICollectionView Diffable Data Source Update Flow 1. Data Update (e.g., fetch, user action) 2. Create New NSDiffableDataSourceSnapshot 3. Apply Snapshot (dataSource.apply) 4. Diffing (system handles) UICollectionView Animated Updates

ItemIdentifierType and SectionIdentifierType Considerations

  • Uniqueness is Key: The Hashable conformance is critical. If your ItemIdentifierType or SectionIdentifierType has properties that can change, ensure your hash(into:) implementation and == operator only consider the stable, unique identifier (like a UUID or a database ID). If you hash mutable properties, changes to those properties will cause the item to be treated as a new item, leading to delete/insert animations instead of reload animations.
  • Structs vs. Enums: Both are excellent choices. Enums are great for a fixed number of sections or item types, while structs offer more flexibility for dynamic data.

UITableViewDiffableDataSource

It's worth noting that UITableViewDiffableDataSource works almost identically to its UICollectionView counterpart. The core concepts of NSDiffableDataSourceSnapshot, Hashable identifiers, and applying snapshots remain the same. If you understand one, you understand the other.

Summary

UICollectionViewDiffableDataSource is a game-changer for UIKit development. It simplifies complex data management, eliminates a common source of bugs, and provides beautiful, automatic animations with minimal effort. By embracing the state-driven approach of snapshots, you'll write cleaner, more robust, and more maintainable collection view code. If you're still using reloadData() or manual index path calculations, now is the perfect time to make the switch!

Happy Swifting!