Skip to Content
Course content

190: Table View and Collection View Diffable Data Sources

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact crash happen in a dozen different PRs over the last few years. You've decided to move away from the old numberOfRowsInSection and cellForRowAt boilerplate and embrace Diffable Data Sources. Everything looks clean, your code is shorter, and then—boom. The app crashes with an "Internal inconsistency" error the moment you try to update the list.

struct Task: Hashable {
    let title: String
    let isCompleted: Bool
}

// ... inside the ViewController ...
var dataSource: UITableViewDiffableDataSource
! func updateTasks(tasks: [Task]) { var snapshot = NSDiffableDataSourceSnapshot
() snapshot.appendSections([.main]) snapshot.appendItems(tasks) dataSource.apply(snapshot, animatingDifferences: true) }

The Identity Crisis in Hashable

If you run this and happen to have two tasks named "Buy Milk," your app is going to crash or behave very strangely. Why? Because you're using the Task struct itself as the identifier in your snapshot. By default, Swift's synthesized Hashable implementation looks at all the properties. But wait—if you have two different tasks that both have the title "Buy Milk" and are both isCompleted = false, they are seen as the exact same item by the diffing algorithm.

The diffable data source doesn't just use the hash to find the item; it uses it to track the item's identity across updates. When the data source sees two identical hashes for different positions in the list, it loses its mind. It can't figure out if an item moved, was deleted, or is a duplicate, leading to that dreaded internal inconsistency exception.

Giving Every Item a Unique Fingerprint

The fix is simple, but it's a conceptual shift. You need to decouple the identity of your data from the values of your data. The most robust way to do this is by adding a unique identifier—usually a UUID—to your model.

struct Task: Hashable {
    let id = UUID() // The unique fingerprint
    let title: String
    let isCompleted: Bool

    // We tell Swift to only use the ID for hashing and equality
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }

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


Now, even if you have ten tasks called "Buy Milk," each one has a unique id. The diffing algorithm can now track exactly which "Buy Milk" task was checked off or moved, and the animations will actually be smooth instead of glitchy.

Managing State with Snapshots

Once you've fixed your identity problem, you can stop thinking about "indexes" entirely. In the old world, you had to calculate IndexPath(row: 4, section: 0) and hope the data hadn't changed since you last checked. With diffable data sources, you just describe the state you want.

I usually recommend creating a helper method to handle your snapshots. Instead of manually appending sections every time, you treat the snapshot as a "truth" document. You define the sections, you throw in your items, and you call apply(). The system handles the heavy lifting of calculating the difference between the current UI state and your new snapshot.

func updateUI(with tasks: [Task], animating: Bool = true) {
    var snapshot = NSDiffableDataSourceSnapshot
() snapshot.appendSections([.main]) snapshot.appendItems(tasks) // This is where the magic happens. Swift calculates the "diff" // and animates only the changes. dataSource.apply(snapshot, animatingDifferences: animating) }

One quick tip: if you're doing a massive initial load of data (like 1,000 items), set animatingDifferences to false. Trying to animate a thousand insertions at once is a great way to make your app stutter during launch.

Handling Selection without IndexPaths

You'll notice that didSelectRowAt still gives you an IndexPath. This is a bit of a legacy hangover. To stay in the "diffable mindset," you should immediately convert that index path back into your item using the data source.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    
    // Don't use myTasks[indexPath.row]! 
    // Use the data source to get the actual item.
    if let task = dataSource.itemIdentifier(for: indexPath) {
        print("Selected task: \(task.title)")
    }
}


By using itemIdentifier(for:), you ensure that you are interacting with the exact object the table view is currently displaying, regardless of how many filters or sorts have been applied to your underlying array.




📋 Practical Task

Build a Dynamic Contact List with Diffable Data Sources

Create a small app that displays a list of Contacts. Each Contact should be a struct with a UUID, a name, and an email.

  • Implement a UITableViewDiffableDataSource to manage the list.
  • Add a "Add Random Contact" button that appends a new contact to your data array and applies a new snapshot to the table view.
  • Implement a "Swipe to Delete" feature that removes the contact from the data source and updates the snapshot.
  • Ensure that the app does not crash when two contacts with the identical name and email are added.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.