Modern table views with diffable data sources
Published on: December 21, 2019At WWDC 2019 Apple announced a couple of really cool features for table views and collection views. One of these cool features comes in the form of UITableViewDiffableDataSource
and its counterpart UICollectionViewDiffableDataSource
. These new diffable data source classes allow us to define data sources for collection- and table views in terms of snapshots that represent the current state of the underlying models. The diffable data source will then compare the new snapshot to the old snapshot and it will automatically apply any insertions, deletions, and reordering of its contents.
In today's article, I will show you how to use UITableViewDiffableDataSource
to drive your table views. Since the table view data source is pretty much the same as the collection view version apart from some class names, I will focus only on the table view variant. The following topics are covered in this article:
- Understanding how a diffable data source is defined.
- Using a diffable data source in your apps.
- Some best-practices to consider when using a diffable data source.
By the end of this article, you will know exactly how to use diffable data sources and what their caveats are.
Understanding how a diffable data source is defined
A diffable data source is an object that replaces your table view's current UITableViewDataSource
object. This means that it will supply your table view with the number of sections and items it needs to render, and it supplies your table view with the cells it needs to display. To do all this the diffable data source requires a snapshot of your model data. This snapshot contains the sections and items that are used to render your page. Apple refers to these sections and items as identifiers. The reason for this is that these identifiers must hashable, and the diffable data source uses the hash values for all identifiers to determine what's changed. Let's look at this a little bit more in-depth by exploring the type signatures of both the data source and the snapshot.
First, let's explore the UITableViewDataSource
signature:
class UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject
where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
That's quite the mouthful! The UITableViewDiffableDataSource
class has two generic types, one for the section identifier and one for the item. Both are constrained so that whatever type fills the generic type must conform to Hashable
. If you're not familiar with generics, check out this post I wrote as an introduction to generics in Swift.
It's interesting that Apple has decided to call the data source's generic parameters SectionIdentifierType
and ItemIdentifierType
. If your data model conforms to hashable, you can use it as the SectionIdentifierType
or as the ItemIdentifierType
. But, as the name of the generic suggests that might not be the greatest idea. I will explain why in the best practices section. For now, what matters is that you understand that both identifiers must conform to Hashable
and that the data source will use hash values to determine changes in your data set.
Now let's look at the second key player in using a diffable data source; the snapshot:
struct NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>
where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
The snapshot object is a struct rather than a class, and it has the same generic parameters as the diffable data source it's applied to. This means that you can't apply a snapshot with a set of identifiers to a data source with different identifiers and your code will fail to compile.
Now that you know how a diffable data source is defined, and have a rough idea of how it works, let's get more practical and see how you can use a diffable data source in your apps.
Using a diffable data source in your apps
In this section, I will use a very simple data model where my section identifiers are integers, and my model contains only a title property. In reality, your models will be much complicated than what I'm using here. However, part of the beauty of diffable data sources is that this doesn't matter. Whether your model is simple or more complex, the principles all remain the same.
Setting up the basics
To create a new diffable data source, you create an instance of UITableViewDiffableDataSource
as follows:
let datasource = UITableViewDiffableDataSource<Int, MyModel>(tableView: tableView) { tableView, indexPath, itemIdentifier in
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath)
// configure cell
return cell
}
In the preceding example, we use Int
and MyModel
as the identifiers for the sections and items respectively. We pass the table view that the diffable data source instance should be applied to the initializer of UITableViewDiffableDataSource
and we also pass it a closure.
This closure is the cell provider. It's called whenever the table view needs a new cell to render on screen. In many ways, this closure is a replacement for the tableView(_:cellForRowAtIndexPath:)
method you might be used to implementing currently. The main difference is that the item identifier that corresponds to the index path for the table view is passed along to this closure. So in the preceding example, you would receive an instance of MyModel
as the third parameter for the cell provider closure. This means that if your identifier type contains all properties that are needed to render your cell, you don't have to fetch this object from your underlying data storage anymore.
Let's create a new snapshot that can be applied to our diffable data source:
var snapshot = NSDiffableDataSourceSnapshot<Int, MyModel>()
snapshot.appendSections(storage.sections)
for section in storage.sections {
snapshot.appendItems(storage.modelsForSection(section), toSection: section)
}
datasource.apply(snapshot)
Note:
The preceding code uses astorage
object. It's not a built-in type or anything but rather a placeholder that I made up to use in this example. I'm sure you can derive what this storage object'ssection
property and themodelsForSection(_:)
method look like. The main point is that you understand how the snapshot is configured.
The snapshot is created as a var
because it's a struct and we wouldn't be able to modify it if we had declared it as a let
. First, we call appendSections(_:)
on the snapshot. This method takes all section identifiers you want to have present in your table view. Then, we loop through all sections and call appendItems(_:toSection:)
on the snapshot. This will associate an array of item identifiers with the section identifier that's passed to this method.
Once all items and sections are added to snapshot, it is passed to the data source by calling apply
.
Note that your data source will not update the table view unless you explicitly create a snapshot and call apply
to update the table view. If you have already applied a snapshot and want to perform a simple add or remove operation of an item or section, you can get the data source's existing snapshot, modify it, and apply it as follows:
var currentSnapshot = datasource.snapshot()
currentSnapshot.deleteItems([itemToDelete])
datasource.apply(currentSnapshot)
The preceding code takes a snapshot of the data source, deletes an item from it and then applies the snapshot to the data source. NSDiffableDataSourceSnapshot
has methods to delete items, sections and even to wipe the entire snapshot clean. If you want to update your data source, it's up to you to decide whether you want to create a new snapshot from scratch or to update the current snapshot.
With a setup like this, you can already provide a table view with data. Let's see how you can add support for section headers and other table view features you might have implemented in the past. For example, cell deletion.
Adding section headers and interactions
Out of the box, the diffable data source is pretty plain. But since it conforms to UITableViewDataSource
it can do anything you might be already doing in your current UITableViewDataSource
implementation. All you need to do is define your own UITableViewDiffableDataSource
and override the methods of features you want to implement. For example, to add section headers you can override tableView(_:titleForHeaderInSection:)
:
class MyDataSource: UITableViewDiffableDataSource<Int, MyModel> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "This is section: \(section)"
}
}
When you subclass UITableViewDiffableDataSource
, you immediately fill in the generic parameters for the section- and item identifiers. In this case, Int
and MyModel
. Note that the section
argument in tableView(_:titleForHeaderInSection:)
is not a SectionIdentifierType
, instead it's always the integer index of your section. Keep this in mind when you construct your section title. If you would run your app and use this data source subclass instead of the regular UITableViewDiffableDataSource
class, you will find that you now have support for section headers.
If you want to add support for deleting items you need to override tableView(_:canEditRowAt:)
and tableView(_:commit:forRowAt:)
in your data source subclass:
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let model = storage.modelForIndexPath(indexPath)
var snapshot = self.snapshot()
snapshot.deleteItems([model])
apply(snapshot)
storage.deleteModel(model)
}
}
Notice how the preceding code finds the model that needs to be deleted in the underlying storage and then updates the existing data source on the snapshot. Finally, the model is also deleted from the underlying storage to ensure that the deletion is persisted.
Best practices for using diffable data sources
During my time exploring diffable data sources, I have run into a problem where I couldn't get cells to reload themselves. Luckily Steve Breen was kind enough to respond to my call for help and I learned a couple of things from him. But even then I was having trouble. Eventually Chris Hinkle came through with an observation. Based on what I've learned I have found two best practices that I think are important to keep in mind.
Keep your identifiers simple and to the point
When you choose your identifiers for your diffable data source and snapshot, try to make sure they only include data that will be rendered or influences the rendering. When I was stuck with reloading my cells, I was using NSManaged
object subclasses to drive my data source. This seems to work well enough because everything was there. Items were added, removed and reordered. However, for some reason, my data source never seemed to pick up changes to the properties of my managed objects. I eventually got around this by providing a struct that contained the data I wanted to render rather than the entire managed object. A nice way I've found to define these structs is as extensions on the models themselves:
extension MyModel {
struct Diffable {
let id: UUID
let title: String
let subtitle: String
// other properties that will be rendered on the cell
init(model: MyModel) {
self.id = model.id
self.title = model.title
self.subtitle = model.subtitle
}
}
}
When using this approach, you'd replace the MyModel
item identifier with MyModel.Diffable
, and when you create your snapshot, you must convert all MyModel
instances to MyModel.Diffable
. Since the initializer for the MyModel.Diffable
object takes an instance of MyModel
this is fairly straightforward:
var snapshot = NSDiffableDataSourceSnapshot<Int, MyModel>()
snapshot.appendSections(storage.sections)
for section in storage.sections {
let items = storage.modelsForSection(section).map(MyModel.Diffable.init)
snapshot.appendItems(items, toSection: section)
}
datasource.apply(snapshot)
By mapping over the array of MyModel
objects using the MyModel.Diffable.init
, every model object will be passed to the MyModel.Diffable
initializer and is converted to an instance MyModel.Diffable
. Pretty nifty, right?
Notice that there is one property in my Diffable
extension that shouldn't be rendered; the id
property. I'll explain why in the next subsection.
Ensure that your identifiers can be uniquely identified
If you don't include any way of uniquely identifying your identifiers, it's really hard for the diffable data source to find out what changed. For example, look at the following code:
let itemsBefore = [{ "name": "Donny Wals" }, { "name": "Donny Wals" }]
let itemsAfter = [{ "name": "Donny Wals" }, { "name": "Donny Wals" }]
The preceding snippet shows two arrays that appear to be identical. And while they might look identical, each object is its own entity. And they might have some kind of underlying ordering and the objects might have swapped positions. It's impossible to tell, right?
The same is true for your diffable data source. If you don't provide it anything unique to identify objects by, it can get confused easily which is probably not what you want. If you include some kind of known unique identifier like a UUID
or an identifier that's used in your back-end, it's much easier to keep track of changes to ordering and individual changes. If you're reading data from a server that you won't modify and you know it already has a sense of uniqueness, for example if every item points to a unique resource on the web, it might be redundant to add an extra identifier yourself.
In summary
I hope I have been able to not only introduce you to diffable data sources, but I hope I have also been able to teach you how you can use them in the real world. Several blog posts I have seen were written shortly after WWDC 2019 and cover the very basics, and unfortunately Apple's documentation this year hasn't been fantastic. I'm sure they will get around to updating it eventually. Luckily, Apple did include some sample code in the links for their Advances in UI Data Sources talk from WWDC 2019.
In today's post, I have shown you how a diffable data source is defined, and how it's used. You saw that a diffable data source has two generic parameters; one for sections and one for items, and you saw that a diffable data source requires a snapshot of your models to render data. I have also explained how you can subclass UITableViewDiffableDataSource
to add support for features like section headers and swipe-to-delete. Lastly, I shared some best practices that you should keep in mind when using diffable data sources.
If there's anything unclear after reading this post, or if you have feedback for me, make sure to reach out on Twitter. I love hearing from you.