
This video is only available to subscribers. Start a subscription today to get access to this and 470 other videos.
Diffable Datasources in iOS 13
Introducing Diffable DataSource
Diffable datasource calculates the difference between two snapshots and applies these changes with an appropriate animation.
Before iOS 13, this required a significant amount of code and often times we would just give up and call reloadData
.
Now with the UITableViewDiffableDataSource
you can create datasource and animate the changes between states.
Creating a Data model
We'll start with an example of grouped episodes by one of their tags. To simulate a delete operation, we'll delete random data using a filter on an episode array and then we'll animate the data before displaying the next set of the table view.
private var episodes: [Episode] = [] {
didSet {
groupedEpisodes = episodes.reduce([:], { (groups, episode) -> [String: [Episode]] in
var newGroups = groups
let tagGroup = episode.tags.sorted().first ?? "xxx No group xxx"
newGroups[tagGroup] = (groups[tagGroup] ?? []) + [episode]
return newGroups
})
}
}
private func munge(_ episodes: [Episode]) -> [Episode] {
return episodes.filter { _ in Bool.random() }.shuffled()
}
@IBAction func refresh() {
if _downloadedEpisodes == nil {
showLoading()
API.fetchEpisodes { episodes in
self._downloadedEpisodes = episodes
self.hideLoading()
self.episodes = self.munge(episodes)
}
} else {
self.episodes = self.munge(_downloadedEpisodes!)
}
}
private var groupedEpisodes: [String : [Episode]] = [:] {
didSet {
tableView.reloadData
datasource.apply(snapshot)
}
}
Conform Your Model to Hashable
The data we are working with has to be Hashable
to work with snapshots. Here each item will contain an ID for identification. In the cases where the item is inserted randomly, you can easily generate random identifiers using UUID. In our case the built-in Hashable
implementation will suffice, but you may have different needs.
struct Episode : Decodable, Hashable {
let id: Int
let title: String
let tags: [String]
}
We have to define what a section is. Sometimes this will be a custom type, but in our case it is just a string. To make the point clear, we'll use a typealias
and use this when referring to sections.
typealias Section = String
Configure Datasource Using UITableViewDiffableDataSource
Next, we'll create a datasource using UITableViewDiffableDataSource
and have a snapshot of the data representing the UI.
private var datasource: UITableViewDiffableDataSource<Section, Episode>!
private var snapshot: NSDiffableDataSourceSnapshot<Section, Episode>!
Here we use the cellProvider
closure to dequeue and configure our table view's cell.
private func configureDatasource() {
datasource = UITableViewDiffableDataSource(tableView: tableView,
cellProvider: { (tableView, indexPath, episode) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = episode.title
return cell
})
}
Creating a Snapshot
We'll replace the reloading of the table view by creating a new snapshot. This snapshot contains all the sections with its grouped items, once ready it is applied to the datasource.
private var groupedEpisodes: [String : [Episode]] = [:] {
didSet {
snapshot = NSDiffableDataSourceSnapshot()
let sections = groupedEpisodes.keys.sorted()
snapshot.appendSections(sections)
for section in sections {
let group = groupedEpisodes[section]!
snapshot.appendItems(group, toSection: section)
}
datasource.apply(snapshot)
}
}
Applying the Grouping to the datasource
Once we add the snapshot to the datasource, the grouping of the data is lost. This might be a bug, but we can restore this behavior by implementing viewForHeaderInSection:
and returning our own view.
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let section = groupedEpisodes.keys.sorted()[section]
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
label.backgroundColor = .systemGray5
label.text = section
return label
}
The downside to doing it this way is that we have to take care to match exactly what the stock header views resemble. With a little more work we can workaround this limitation and use stock headers (albeit with some boilerplate code).
Restoring the Stock Header View Behavior
To restore this behavior, we'll set the tableView's dataSource back to self
. We'll then implement and pass behavior off to the datasource
we created earlier. This gives us control of returning a string again for titleForHeaderInSection:
.
private func configureDatasource() {
datasource = UITableViewDiffableDataSource(tableView: tableView,
cellProvider: { (tableView, indexPath, episode) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = episode.title
return cell
})
//Uncomment this to restore the stock header view behavior
tableView.dataSource = self
}
// Comment this to restore the stock header view behavior
//override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// let section = groupedEpisodes.keys.sorted()[section]
// let label = UILabel()
// label.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
// label.backgroundColor = .systemGray5
// label.text = section
// return label
//}
// Uncomment these to restore the stock header view behavior
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let section = groupedEpisodes.keys.sorted()[section]
return section
}
override func numberOfSections(in tableView: UITableView) -> Int {
return datasource.numberOfSections(in: tableView)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return datasource.tableView(tableView, numberOfRowsInSection: section)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return datasource.tableView(tableView, cellForRowAt: indexPath)
}
So now when we refresh the episodes we'll see an animated view with groups added over the section.