With the release of iOS 13, Apple introduced UITableViewDiffableDataSource
for table views. This class eliminates the need for UITableViewDataSource
and is a great way to dynamically update your table view’s data. The major benefit of this class is that you don’t have to deal directly with index paths when inserting or removing items (no more dreaded index-path-out-of-bounds crashes!). You also don’t have to feed in the number of rows and sections to the table.
Once the diffable data source is set up, it’s just a matter of applying a snapshot of the underlying data whenever it’s updated. The table view handles all of the inserting/removing of rows automatically.
The following example code will be inside a view controller, which I’ll post the entirety of at the end. It will be basic contact list. Keep in mind this is a simple use case, and I would highly encourage you to check out the documentation for UITableViewDiffableDataSource
and NSDiffableDataSourceSnapshot
to see all the possible things you can do.
First off, we’ll add the diffable data source:
var tableDataSource: UITableViewDiffableDataSource<UITableView.Section, Contact>!
We specify types for two generics: the section and item we want to use for the underlying data (in this case, Contact). Both of these types must conform to Hashable
. For the section, although you can use any hashable type you’d like (even Int), I like to specify sections in an enum. I’ll usually throw this in an extension on UITableView
.
struct Contact: Hashable {
var id: String
var name: String
var email: String
}
extension UITableView {
enum Section {
case main
}
}
Next, we need to initialize the diffable data source we declared. Since this is being force-unwrapped, we’ll want to do this right away, so it will go viewDidLoad()
.
override func viewDidLoad() {
super.viewDidLoad()
setUpTableDataSource()
}
func setUpTableDataSource() {
tableDataSource = UITableViewDiffableDataSource<UITableView.Section, Contact>(tableView: tableView, cellProvider: { tableView, indexPath, contact in
let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell")
cell?.textLabel?.text = contact.name
cell?.detailTextLabel?.text = contact.email
return cell
})
}
In the initializer for UITableViewDiffableDataSource
, we pass in the table view we’re using it with, as well as a closure where we configure and return our cell. You’ll notice the closure arguments look very similar to the tableView(_:cellForRowAt:)
method we’d otherwise be using in data source delegate. The beauty of this is we get access to the contact item directly in the closure, without having to do a lookup by index on the source data.
Next, we’ll create a method to apply a snapshot of our data to the data source. We’ll call this whenever our data is updated in any way, to let the table view know it’s been updated and to insert or remove any rows it needs to.
func updateSnapshot(animated: Bool = true) {
var snapshot = NSDiffableDataSourceSnapshot<UITableView.Section, Contact>()
snapshot.appendSections([.main])
snapshot.appendItems(contacts)
tableDataSource.apply(snapshot, animatingDifferences: animated)
}
You’ll see the snapshot has the same types for the generics as the data source. After we initialize the snapshot, we append our section then our items. We apply the snapshot to the table view, specifying if the change should be animated.
We’ll go ahead and add a call to the snapshot update right after we set up our diffable data source. This will make the table view display our data right away after setup.
func setUpTableDataSource() {
tableDataSource = UITableViewDiffableDataSource<UITableView.Section, Contact>(tableView: tableView, cellProvider: { tableView, indexPath, contact in
let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell")
cell?.textLabel?.text = contact.name
cell?.detailTextLabel?.text = contact.email
return cell
})
updateSnapshot()
}
Now we can add other methods to add and remove contacts:
func deleteContact(id: String) {
guard let index = contacts.firstIndex(where: { $0.id == id }) else { return }
contacts.remove(at: index)
updateSnapshot()
}
func addContact(_ contact: Contact) {
contacts.append(contact)
contacts.sort(by: { $0.name < $1.name })
updateSnapshot()
}
That’s really all there is to it! There are other methods that can be used on the snapshot, such as if you want to just reload specific rows. As I said above, I recommend checking out the docs for more. Diffable data source is also available for collection views.
Here is the full code of the view controller used in this article.