Daily coverage of Apple’s WWDC 2019 conference, by John Sundell.

A Swift by Sundell spin-off.

A first look at the new diffable data sources for table views and collection views

While SwiftUI has definitely stolen much of the spotlight during this year’s WWDC — by being the shiny new UI framework for all of Apple’s platforms — it’s far from everything that’s new in terms of UI development this year.

Some incredible advancements have also been made within the existing UI frameworks, such as the new built-in, diffable data sources for table views and collection views — which could potentially eliminate a whole class of very common bugs found in many different apps.

New API conventions

The theme of this year’s WWDC (besides, you know, “🤯”) seems to be declarative UI programming — and it’s not that surprising to see Apple moving many of their APIs (even existing ones), towards a more declarative world — because declarative APIs do have a number of really great benefits.

One such benefit — arguably one of the major ones — is that declarative APIs tend to reduce the opportunity for errors to occur, by reducing statefulness. When an app has less state to keep track of, fewer bugs tend to appear — since the risk of various states ending up out-of-sync is heavily reduced.

A first look

A very common source of such out-of-sync bugs have historically been UIs based on either UITableView or UICollectionView — both of which that are fundamental parts of UIKit (and on the Mac NSCollectionView is growing in popularity as well). So this year, Apple is outfitting both of these classes with brand new data source APIs — which come together as a big step forward in terms of developer experience, robustness, and efficiency.

Let’s take a look at an example of a fairly straightforward UITableView-based view controller, which displays a list of contacts by subclassing UITableViewController:


class ContactListViewController: UITableViewController {
    private let cellReuseIdentifier = "cell"
    private lazy var dataSource = makeDataSource()

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(SubtitledCell.self,
            forCellReuseIdentifier: cellReuseIdentifier
        )

        tableView.dataSource = dataSource
    }
}
                    

As you can see above, we’re creating a dedicated data source object by calling a private makeDataSource() method, which we’re then assigning as our table view’s dataSource.

Before this year’s SDK changes, such a data source would have to be a manual implementation of the UITableViewDataSource protocol — that’d define how many rows our table view should have, and how to configure cells for it.

However, that’s no longer the case — since we can now use the brand new UITableViewDiffableDataSource to create a data source that simply uses a cellProvider closure to dequeue and configure our table view’s cells — like this:


private extension ContactListViewController {
    func makeDataSource() -> UITableViewDiffableDataSource<Section, Contact> {
        let reuseIdentifier = cellReuseIdentifier

        return UITableViewDiffableDataSource(
            tableView: tableView,
            cellProvider: {  tableView, indexPath, contact in
                let cell = tableView.dequeueReusableCell(
                    withIdentifier: reuseIdentifier,
                    for: indexPath
                )

                cell.textLabel?.text = contact.name
                cell.detailTextLabel?.text = contact.email
                return cell
            }
        )
    }
}
                    

That’s already eliminating quite a lot of the boilerplate that was previously required in order to build either table views or collection views — but that’s just the tip of the iceberg.

Above, you may have noticed that we specify two generic types when we create our UITableViewDiffableDataSourceSection and Contact. The first of those types, Section, is the type that’ll be used to identify sections within our table view. That’s right — the Int-based section indexes have been replaced by type-safe identifiers.

Along those same lines, the reason we specify our model type — Contact — as the second generic type, is because our data source will use those values to uniquely identify each cell and its corresponding data. No more passing around index paths — and again improving type safety.

Let’s take a look at those two types. First we have our Section type, which is a standard issue enum that contains cases for each of our table view’s three sections:


extension ContactListViewController {
    enum Section: CaseIterable {
        case friends
        case family
        case coworkers
    }
}
                    

Second, our model — which contains the name and email address of one of our contacts — and has been made Hashable in order for our diffable data source to be able to uniquely identify it. We’ve also created a ContactList model that contains all of a user’s contacts, grouped by type:


struct Contact: Hashable {
    var name: String
    var email: String
}

struct ContactList {
    var friends: [Contact]
    var family: [Contact]
    var coworkers: [Contact]
}
                    

With our setup complete, let’s now take a look at the actual core functionality of the new diffable data sources — diffing!

Automatically diffed updates

Previously, updating a table view or collection view usually involved either reloading the entire view completely (which isn’t very efficient, or elegant, for partial updates), or using the batch update API. The latter is an incredibly powerful API, but it’s also really hard to use, and could easily lead to bugs and crashes when updates were either performed in the wrong order — or when the underlying data ended up changing during an update.

But now, updating our table view is just a matter of constructing a snapshot of its current data, and passing that directly to our data source by calling its apply() method. That’s it! No batch updates, no fiddling around with index paths, none of that stuff!

Here’s how we could use that new API to create an update() method, that’ll simply take all the contacts within a ContactList, turn those into a snapshot, and then apply that snapshot to our table view:


extension ContactListViewController {
    func update(with list: ContactList, animate: Bool = true) {
        let snapshot = NSDiffableDataSourceSnapshot<Section, Contact>()
        snapshot.appendSections(Section.allCases)

        snapshot.appendItems(list.friends, toSection: .friends)
        snapshot.appendItems(list.family, toSection: .family)
        snapshot.appendItems(list.coworkers, toSection: .coworkers)

        dataSource.apply(snapshot, animatingDifferences: animate)
    }
}
                    

When doing the above, our UITableViewDiffableDataSource will take the snapshot we constructed, diff it — and then update our UI with the resulting changes — all automatically, including calculating the most appropriate animations to use.

The beauty of this new API, and what makes it more declarative, is that we no longer have to keep track of our table view’s UI state — we can simply tell our data source what to render, and it’ll figure out how to efficiently get there, all by itself.

We can also easily define partial updates to our UI, by grabbing a copy of our data source’s current snapshot (by calling the snapshot() method on it), modifying it, and then passing it back through apply() — like in this example, where we build a method that lets us remove a contact from our list:


extension ContactListViewController {
    func remove(_ contact: Contact, animate: Bool = true) {
        let snapshot = dataSource.snapshot()
        snapshot.deleteItems([contact])
        dataSource.apply(snapshot, animatingDifferences: animate)
    }
}
                    

Really cool! 😎 There’s also UICollectionViewDiffableDataSource (iOS/tvOS) and NSCollectionViewDiffableDataSource (macOS) for collection views, which work very much in the same way.

Conclusion

Diffable data sources is a huge leap forward in terms of how easy both table views and collection views are to work with — and how stable the implementations we build on top of them are likely to become. By providing a more declarative API that moves much of the complexity of dealing with UI state into UIKit itself, a huge class of mistakes and bugs can be avoided — most likely resulting in fewer crashes, and better-performing apps.

What do you think? Will you adopt these new diffable data sources in your app, and what’s your first impression of this new set of APIs? Let me know on Twitter @johnsundell.

Thanks for reading! 🚀