WWDC 2019 was really packed with new stuff for iOS and the new data sources for TableViews and CollectionViews didn’t get the same attention as SwiftUI but are a really nice addition for people still working with UIKit.
I’m going to focus my post on UITableViewDiffableDataSource but there is a similar API for CollectionViews and if you want to find out more there’s a great session you can watch at this link.
As usual you can find all the code in a sample project on GitHub
The old way
Before iOS 13 there were two ways to update a TableView with new data. The first, not really efficient one, was to call reloadData and have the entire TableView reloaded.
The second one was calling beginUpdates and endUpdates and put operations between those calls, or introduced in iOS 11 the API performBatchUpdates.
The problem with that API is performing all the operations in the right order, and with the underlying data changing at the same time.
New APIs
Fortunately Apple has released a new set of APIs in iOS 13 to deal with this kind of problem. No more we have to either reload the entire table or perform batch updates in the correct order, now there is a system API to do the same thing, but is not necessary to specify the operations to perform on the data set, the API will future it out for us.
It is possible to perform an update on a set of data by defining a snapshot and the underlying API will determine the diff and perform the operations on the TableView.
Example
I think a good example is performing a search on a list of data. While the search is performed is great to have the cells animated so the ones matching the search remain visible while the others disappear from the list. I’m using a public API with a list of beers, and I have a struct Beer with name, description etc. I want to display on a table.
For this example I’m using a subclass of UITableViewController, but you won’t see any of the function of UITableViewDataSource, only the ones from the delegate. The data source is replaced by creating an instance of UITableViewDiffableDataSource. You can see the code here
private func configureDataSource() {
tableDataSource = UITableViewDiffableDataSource<Section, Beer>(tableView: self.tableView, cellProvider: { (tableView, indexPath, beer) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: self.cellIdentifier)
cell?.textLabel?.text = beer.name
cell?.detailTextLabel?.text = beer.description
return cell
})
}
As you can see we can fill the table view cell directly while creating the data source via a closure, that’s super convenient. As you can see we have to specify a two types: the first one for the sections and the second for the cells. They need to be Hashable, so I used an enum for the section and made the Beer struct conform to Hashable. Note that he closure gives us the TableView, an IndexPath and the Beer record. We don’t have to look for the beer into an array of beers with indexPath.row, it is already available.
So far so good, but how do we populate the TableView? We have to create a snapshot like I mentioned before
private func updateDataSource(withBeers beers:[Beer], animated:Bool) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Beer>()
snapshot.appendSections([.main])
snapshot.appendItems(beers, toSection: .main)
tableDataSource.apply(snapshot, animatingDifferences: animated)
}
Just like UITableViewDiffableDataSource NSDiffableDataSourceSnapshot requires two types conforming to Hashable to be initialised. How do we create a section? By appending the Section type to the snapshot, and the following line puts items into a particular section. That’s it, no more calls to the data source methods to get the number of sections and the number of items into a section. When we’re done with the snapshot we can apply it to the data source. There is a parameter to animate the changes, at startup I set it to false while during a search I pass true so we can see a nice animation while typing.
We saw the data source, what about the delegate? There are no new APIs, but as you saw we didn’t work with IndexPaths so how do we get the selected item? The new data source takes care of that for us
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let beer = tableDataSource.itemIdentifier(for: indexPath) {
print("selected beer \(beer)")
}
}
itemIdentifier(for:) allows us to get the item associated with the selected IndexPath. We’re talking about the item found in the current snapshot, so even if the array of Beer changes over time we are fine as long as we update the table view via a snapshot, which you can retrieve at any time by the data source object if you need to. There is also a function to get an IndexPath from an element.
This was just a quick introduction to the new diffable data source, I didn’t cover the APIs for UICollectionView but they’re very similar to the one you saw in this example. Happy coding!