Diffable Data Sources
This article talks about a new approach to create data sources for Tableview / Collectionview called Diffable Datasource which was introduced this year in WWDC.
Let’s see first how we implement your tableview datasource currently.We set a class as datasource for our tableview / collectionview and then we implement two required datasource methods as follows to provide the number of cell and the cell that we want to show.
extension SampleVC: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { models.count() } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cellIdentifier = String(describing: SampleTableViewCell.self) guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as? SampleTableViewCell else { fatalError("Cell with given identifier not found...") }
.
.
. return cell }}
Benefits of this approach
- Simple — Just implement two required datasource methods and that’s it.
2. Flexible — You don’t really have to used any particular kind of data structure to back your data source.It could be simple as 1-D array and if you have multi-sectioned data source then it could be a 2-D array.
Problem with this approach
Apps are often little more complicated than 1-D array or 2-D array and apps get more and more complex every year. They do more things as users demands more features and often times these datasources are backed by complex controllers inside of our app and these controllers can do variety of things such as interacting with core data or talking to web service just to name a few.
Now consider a scenario where UI layer and controller are interacting with each other. So UI layer starts the interaction by asking the number of rows and the row to display from the controller.Controller provides this data and till now everything is good. The controller also has a web service and it interacts with the web service to fetch new data.So after fetching the new data it informs the UI layer about the new changes it has. Now it’s up to the UI layer to decide as things changed and now it has to update itself with the new data. This involves mutations against tableview / collection view which could be bit complex. This results into reload / batchupdates and mutation of the backing source.
But sometimes no matter how hard you try, things go wrong like following
Why this problem occurs ?
There are data controllers also acting as data source, has a version of truth which changes overtime and UI has a version of truth and UI layer code is responsible for mitigating that, making sure that it is always in sync, but as we saw above that sometimes it is hard. So the current approach is error prone and it is primarily due to the fact that there is no notion of centralised truth.
Note — The above problem comes when you performBatchUpdates() on tableview or collectionview and one way to fix this crash is to use reloadData() instead of performBatchUpdates() but that will result into non-animated data reload effect and that results into no so good user experience.
The New Approach — Diffable data source(iOS, tvOS, macOS)
This year in WWDC apple has introduced diffable datasources and with that they introduced four new classes
UICollectionViewDiffableDataSource, UITableViewDiffableDataSource (iOS and tvOS)
NSCollectionViewDiffableDataSource (macOS)
NSDiffableDataSourceSnapShot (Common to all platform)
This diffable datasource has something called snapshot.
SnapShot — It’s a simple idea and is effectively truth of the current UI state and instead of index path it’s an association or collection of section identifiers that are unique and item identifiers and you update these with identifiers and not with index path. It is a generic class in swift and is parameterised by the SectionIdentifier type and ItemIdentifier type that we have decided to use. Both SectionIdentifier type and ItemIdentifier types needs to be unique and for swift this needs to conform to hashable.
UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>
SectionIdentifier type — For common cases where you have only single section you can use this trivial technique where you can declare an enum with single case. One nice thing about enums in swift is that they are automatically hashable.
enum Section { case main}
You can have multiple cases in this enum as per the number of section required in the tableview.
ItemIdentifier types — Here we will use a custom type. So here in the example below we are using a swift struct and that struct is declared as hashable so that we can use it with diffable datasource. Considering that we have a requirement that each custom type must be uniquely identifiable by its hash value, we will achieve this uniqueness by giving each custom type an automatically generated unique identifier and by implementing the hashbility conformance. So in this way we will have each custom type and it’s hash value (specifically of the identifier) unique enough for diffable datasource to track from one update to next .
Here’s how you create a custom ItemIdentifier type
struct Model: Hashable { let identifier: UUID = UUID()
.
.
. func hash(into hasher: inout Hasher) { hasher.combine(identifier) } static func == (left: Movie, right: Movie) -> Bool { left.identifier == right.identifier }}
How to implement this diffable datasource for a tableview.
It’s a 3 step process
- Create a snapshot.
var snapshot = NSDiffableDataSourceSnapshot<Section, Model>()
This snapshot is initially empty so it is up to us to populate it with sections and items that we want.
2. Populate that snapshot with the description of items that you want to display in that update cycle.
snapshot.appendSections([.main]) — Here we have only one section to display.
snapshot.appendItems(Model.data, toSection: .main) — We append the identifiers for the items that we want to display in this update. We usually pass an array of identifiers here but in swift you can also make things a lot more elegant by working with your own native types here. So if you have a native type and it can even be value type such as struct or enum, if you make that type hashable then you can pass your own native objects in terms of swift syntax if that’s what you are doing.
3. Apply the snapshot to automatically commit the changes to your UI. Diffable datasource take care of the the diffing and all the changes to the UI elements.
dataSource.apply(snapshot, animatingDifferences: true) — Diffable datasource goes off and automatically figures out what changed between previous update and the next. Notice that there is no code at all here where we had to stop and figure out the reason about what we were displaying previously in the update cycle and all UI updates happens automatically.No need to deal with indexpaths which are fragile and ephemeral because they refer to a certain particular update and have a different meaning in a different update.We are dealing with identifiers that are robust and enduring.
How to configure a diffable datasource ?
private func configureDatasource() { dataSource = UITableViewDiffableDataSource<Section, Model> (tableView: self.tableView, cellProvider: { (tableView, indexPath, model) -> UITableViewCell? in let cell = tableView.dequeueReusableCell(withIdentifier: self.cellId, for: indexPath) .
.
. return cell })}
Here we are passing a pointer to the tableview with which we want to work. Diffable datasource takes this pointer and wire itself up as datasource of that tableview so there nothing else for us to do. In the trailing closure above you have to pass the tableview cells i.e. all this is the code that you would normally write in cellForRowAtIndexPath: method.
One nice thing about this trailing closure syntax is that that along with the index path we also get our custom type here, so no more querying our data model with indexapth for the data to show in the cell.
Few considerations
- Always call apply() and don’t call performBatchUpdates(), insertItems()…etc and if you call performBatchUpdates(), insertItems() then framework will complain(log or assert).
- Two ways to create snapshots
1.Empty snapshot
let snapshot = NSDiffableDataSourceSnapshot<Section, UUID>()
2. Current data source snapshot’s copy — This is useful when certain actions occurs and you have to modify one little thing like deleting an item from a datasource like in the following example
var currentSnapShot = dataSource.snapshot()
currentSnapShot.deleteItems([itemIdentifier])
dataSource.apply(currentSnapShot, animatingDifferences: true)
This creates a separate copy of the snapshot and any changes to this will not affect the datasource from which it came from.
- Identifiers — This has to be unique. For swift this needs to be conforms to the hashable and conveniently enough many things in swift conforms to hashable like enums types. You can bring in some model data into these identifiers and this is really convenient now as your identity needs to come from some identifier but you can also bring in other attributes like name and this is really handy because when you configure your cell you will have everything you need right there inline and no need to look somewhere else.
What about the API’s that used index path ?
As this new diffable datasource api is moving away from indexpath to identifiers i.e. SectionIdentifiers and ItemIdentifiers, so how this will affect the existing API’s that still uses indexpaths ?
The answser to that question is following
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if let selectedItemIdentifier = self.dataSource.itemIdentifier(for: indexPath) {
.
.
. }}
Use the above mentioned API to convert the indexpath into itemIdentifier.
Performance
Diffs are liner operations o(n) which means the more items you have longer diffs will take. So during development it is super important to measure your apps to make sure that main queue is always as free as possible to be responsive to the user events and everything rendered really quickly. So in cases you find that you have large number of items and that liner diff is taking bunch of your time then it is safe to call the apply() method from the background queue.
What happens when you call the apply() from the background queue?Framework is smart enough to realise that it is not on the main queue so it will keep on diffing in the background and once diff is computed it will switch back to the main queue and apply the results from the diff.
Make sure that if you choose this model to call apply from the background queue then be consistent with it. Don’t do mix and match by calling it from the main queue and background queue both. Always do it in the same way. In case you use the mix and match framework approach then framework will complain(log or assert) about this.
Conclusion
Content mentioned in this article is taken straight from the WWDC 19 session called advances_in_ui_data_sources and i would highly recommend you to watch this. Also if in any part of this article you felt lost then again go and watch this video first and i am sure all you doubts will be answered.
That’s it for now and i hope you find this helpful. Thanks for reading. 😀😀😀
Resource for this article:
https://developer.apple.com/videos/play/wwdc2019/220/
Source Code -https://bitbucket.org/VAnand_02/vatableviewwithdiffiabledatasource/src/master/