-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
190: Table View and Collection View Diffable Data Sources
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
Taskstruct itself as the identifier in your snapshot. By default, Swift's synthesizedHashableimplementation looks at all the properties. But wait—if you have two different tasks that both have the title "Buy Milk" and are bothisCompleted = 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
animatingDifferencestofalse. 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
didSelectRowAtstill gives you anIndexPath. 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
UITableViewDiffableDataSourceto 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.
There are no comments for now.