Magic of Diffable Tableview Datasource
It was in WWDC 2019, apple decided to improve UI datasources. That day, developers got introduced to new diffable datasource api for tableviews and collections views.
https://developer.apple.com/videos/play/wwdc2019/220/
History
If you have worked on tableviews, then you must have come across this famous error on your console.
** ‘NSInternalInconsistencyException’, reason: ‘Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).’ **
When this error appears on your console, you would have even searched this error in your browser . Thats fair right!. We may not exist without StackOverFlow.
Well this is due to inconsistency with your datasource. Solution is to first update your datasource model, then apply insert or delete operations on the tableview’s datasource with respect to updated indexes. Even though you know the solution, this scenario will happen un-knowingly and error prone, since you have to know when to update or invalidate the datasource. Many of them would have chosen the easy solution, ‘tableview reloadData’ when you found the complexity in just reloading specific row or section, inserting new rows to the section, or deleting complete section from the tableview.
Yup! You guessed it right. We have this sweet solution in our app.
Future
Now to solve the above issue, apple has introduced a new datasource api in iOS13, which can be used with either tableview or collectionView. And this api is smart enough to identify the differences between your old and new datasources into the tableview. Using UITableViewDiffableDataSource or UICollectionViewDiffableDataSource, would solve most of tableview nightmares. Let’s just focus on tableview diffable datasource.
Implementation
As you know UITableView provides protocols for datasource and delegate. Let’s just focus on datasource here. UITableViewDataSource, a protocol which defines api’s to prepare data for the tableview. The consuming view controller or tableview controller should implement those protocol methods. If you decide to use new diffable datasource, then these api’s becomes obsolete.
(Remember these api’s are not deprecated, since we can still use old way datasource)
@available(iOS 2.0, *) func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
@available(iOS 2.0, *) func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
@available(iOS 2.0, *) optional func numberOfSections(in tableView: UITableView) -> Int
Diffable Datasource API
// create diffable tableview datasource
private func makeDatasource() -> UITableViewDiffableDataSource<SectionModel, RowModel> {
let reuseIdentifier = rowIdentifier
return UITableViewDiffableDataSource<SectionModel, RowModel>(tableView: tableView) { tableView, indexPath, rowModel -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
cell.textLabel?.text = rowModel.name
cell.detailTextLabel?.text = rowModel.detail
return cell
}
}
In old tableview, you would have set the datasource to self. So that the view controller has to implement those datasource api’s.
In the diffable datasource, we have to create a UITableViewDifableDataSource(<Section where Hashable>, < Row where Hashable>, <Cell Provider closure>)
- Section where Hashable, a first argument to identify our section types in the datasource. We can create any data structure, but it should conform to Hashable protocol, so that iOS code internally uniquely identifies each section.
If we use default data types like enum, it by default conforms to hashable protocol. But if we use custom data types like our section model struct, then we should conform to hashable protocol and implement
func hash(into hasher: inout Hasher) {
hasher.combine(uniqueId) // here you can use any value, which helps you to uniquely identify this data type.
}
2. Row where Hashable, it is again same as section. It is used to identity rows in the datasource.
3. Cell Provider where UITableViewCell, as opposed to old approach, where we used cellForRow to dequeue a cached row and configure it with data. In this cell Provider, we write a closure which request tableview cell and configures with the row model.
To update the datasource with new data or model,
// update datasource with new items
func update(with cardsViewModel: CatalogCardsViewModel, animate: Bool = true) {
var snapshot = NSDiffableDataSourceSnapshot<SectionModel, RowModel>()
cardsViewModel.cards.forEach { (section) in
snapshot.appendSections([section])
snapshot.appendItems(section.rows, toSection: section)
}
datasource.apply(snapshot, animatingDifferences: animate, completion: nil)
}// delete items from datasource
func remove(_ card: RowModel, animate: Bool = true) {
var snapshot = datasource.snapshot()
snapshot.deleteItems([card])
datasource.apply(snapshot, animatingDifferences: animate, completion: nil)
}
We can use snapshot to update datasource, if it is for the first time, then create a snapshot and apply it to tableview datasource. If the snapshot is already there, then get the snapshot from the datasource and update it by either adding or by removing section/row items.
Advantages
In the old tableview datasource api, we used to have two copies of datasources. One is inside tableview, which is not accessible and other is what we maintained in our view models. Therefore it was necessary to keep both datasources in sync to avoid any inconsistencies.
We had to explicitly handle index overflow or underflow for the datasource models, when we were indexing.
Tableview datasource snapshot holds the information of section and row models.
let sectionModel = datasource.snapshot().sectionIdentifiers[section]let rowModel = datasource.snapshot().sectionIdentifiers[indexPath.section].rows[indexPath.row]
No more code like this,
func getRowProvider(for row: Int) -> RowDataProviding? { guard row >= 0 && row < rows.count else { return nil } return rows[row]}func getSectionDataProvider(for section: Int) -> SectionDataProviding? { guard section >= 0 && section < sections.count else { return nil } return sections[section]}
Isn’t this amazing. Yup it is!!
You can explore some of the convenience api’s available in these structs
struct NSDiffableDataSourceSectionSnapshot<ItemIdentifierType> where ItemIdentifierType : Hashablestruct NSDiffableDataSourceSectionTransaction<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashablepublic struct NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
Finally, these new api’s definitely makes using TableViews pretty easy than before.
Hurray!! This is my first story in Medium. I was so inspired by the ios community here in Medium. I would look forward to bring in more interesting stories with different approaches and thoughts.
You can visit my git repo here