iOS 13 — Be Dynamic with DiffableDataSource

Overview

“attempt to insert row 144 into section 0, but there are only 0 rows in section 0 after the update”.

I think there isn’t an iOS developer that hasn’t seen this crash before. Finally, after more than 10 years, Apple decided to give a decent solution to one of the most frustrating issues we have on tables and collection views — animating changes to the collection.

Apple mixed a little bit of SwiftUI Concept here

It’s not a coincidence DiffableDataSource is released alongside SwiftUI. While the old good UITableViewDataSource protocol that is based on two methods — cellForRow and numberOfRows, is dealing with only cells and indexPath’s, DiffableDataSource is attached to the data itself and can distinguish between the data items with the help of Hashable protocol.

Just like SwiftUI, DiffableDataSource has a mechanism of applying changes while calculating the differences in the data for you and transition the UI to the new state with nice animation almost automatically.

Applying changes nicely it’s just the tip of the ice

Apple is selling us the DiffableDataSource as a solution for “insertRowsAtIndexPath” crashes issues, but in fact, it changes the whole way collections work with data, starting with preparing the data items, creating the cells, reloading specific cells and up to working with external services that refresh your collection.

When you first approaching DiffableDataStructure, you need to forget how UITableDatasource / UICollectionViewDatasource works for a second, because DiffableDataStructure is an entirely different thing.

These are the main steps you need to do when using DiffableDataSource:

  1. Prepare data items, that conform to Hashable protocol.
  2. Create a data source that generates the cells for the collection
  3. Create a snapshot and fill it with the data items.
  4. Respond to changes by modifying the snapshot and apply it to the data source.

Data Items

Snapshots have to work with data items that conform to Hashable to do its magic. This may sound trivial, but it’s not always the case. Sometimes, developers create more dynamic data sources that rely on some logic instead of a list of structs or objects. For instance — “if indexPath.row == 0 { // show something // )” is a common use case. Here we need to create data items and define the data source and snapshots to rely on their type.

https://gist.github.com/AviTsadok/2115f33e0b122d5fad8224aa96ac7a95

Since enumeration member values and swift basic types (String, Int..) all conform to Hashable protocol, it’s not that difficult to accomplish this mission

New Datasource

So the data source object replaces the object that conforms to UITableViewDataSource, and it’s very easy to use it.

All you need to do is:

  1. Declare the two types for the data source — the section type and the row type.

2. Allocate the data source and in the closure just return the cell according to the row type.

3. There is no step 3. That’s it 🙂

https://gist.github.com/AviTsadok/24fce4d05208588075edcaa5ec303ad1

Keep a reference to this data source because you’re gonna need it to apply snapshots.

Working with Snapshots

https://gist.github.com/AviTsadok/4db085fd04d4bcf23a2d6f380eeb1126

We created the data source, but we haven’t given yet any data to work with. So, If you want to fill the data source, a snapshot is the way to go.

Create a NSDiffableDataSourceSnapshot that works with the same types of the data source, append items to the snapshot and apply the changes in the data source.

Snapshots can do more than add items and sections. They can also move, delete and reload items in your data source.

The working method is that you can take the current snapshot displayed in the data source and modify it, or you can create a new snapshot and apply it to your data source.

Also, you don’t have to keep references to the objects you pass to your snapshot — once you passed them, your data source and snapshot objects have references to them.

Now, let’s talk about common use cases and how to implement them with snapshots.

Create initial data for your collection:

To show data, your data source object needs a snapshot. Create a snapshot, add data to it and add it to your data source.

Note — snapshots require at least one section to work, so just add an enum value as a section in case your data is flat.

General, it’s best to create a function that takes both a data structure and a snapshot and fill the snapshot with items.

Add, delete or move items in the snapshot:

To make changes to the current list, you don’t have to create a new snapshot. Just retrieve the current snapshot from the data source (datasource.snapshot()), and make the relevant changes.

If you want to add an item to the snapshot, use a snapshot.appendItems() and pass the list of items and the section.

If you want to insert an item to a specific location in the list, use snapshot.insertItems.

To delete items, use snapshot.deleteItems() and pass the list of items to delete.

I want to reload specific cells

Reloading a cell though a little bit tricky. You can pass a modified item to the snapshot, but when the closure that generates the cell will run, it will pass the old item (I don’t know if it’s a bug or but it’s by design but this is how it works). The solution is always to keep an updated data store and fetch the item by it’s ID before passing to the snapshot.

I want to replace the current data with new data

If it’s too complicated to identify the new changes or you just want to reload the collection with new data, just create a new snapshot, fill it with what you want and apply it to the data source. If there are items in the current snapshot that exists in the new snapshot, the datasource is smart enough to apply the changes with a nice animation.

What about responding to selecting items, layout…?

We said DiffableDataSource replaces only the UITableViewDataSource. Use can still use UITableViewDelegate alongside DiffableDataSource, and implement didSelectRow just like you did before.

Calculating changes seems like a heavy task. Can I call it from a background thread?

Yes, you can! I’m not sure it’s needed though, because it seems like Apple engineers did a great job here. All you need to remember is to make it consistent — if you call it from a background thread, just make sure all the calls to the specific data source is being called from the background thread.

I need to support iOS 12

Welcome to reality — In the following year you probably will need to support iOS 12 users, so some tips for back compatibility:

Set UITableViewDataSource to the table view only for iOS 12, otherwise, the table view will work with two data sources in iOS 13, and that’s not something you want.

Move UITableViewDataSource to a different class. It’s not mandatory, but it’s better to make your life simpler when you need to support 2 different API’s.

If animating changes in iOS 12 is too complicated for you, just use UITableView.reloadData() method, and leave a better experience for iOS 13 users. Adoption of new iOS 13 versions is very fast, and in a few months iOS 13 will be the most popular iOS version.

So, that’s the end of stability issues with UITableView’s, right?

Well, almost. As long as you take actions that make sense, you are safe. But you can still crash the app with DiffableTableView.

Some examples of how to crash it:

  1. Add items to non-exists section
  2. Delete Item and then reload it.
  3. Add items when you don’t have sections at all.
  4. Move items that are not in your snapshot.

I can think of more examples, but you get. Don’t try it on App Store 🙂

Summary

DiffableDataSource can surely make your life easier and give your users a better user experience. Some challenges come with it — you need to adapt your app pattern (MVVM/MVP/VIPER/MVC) and see how it integrates with the new API, make changes to your model and of course, still support iOS 12 at least for the next year.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s