Since iOS 13, we’ve been able to revamp the way our table and collection views work by using diffable data source classes (UITableViewDiffableDataSource and UICollectionViewDiffableDataSource respectively) and snapshots. This provides the advantage of being able to write less code for these types of views, while making our state management much more robust and less likely to fail with the dreaded NSInternalConsistencyException
error.
While there are numerous articles and blog posts around about the typical use case for using diffable data source, a homogenous data set with identical table cells, I found very little on the more real world scenario where a table might have two or three different data types and possibly an ’empty data’ cell as well. We’ll be looking at one approach to solving this problem here today.
Setting Up Our Data and View
For our simple scenario, we’ll just assume that we’ve got a fruit stand selling apples and oranges. Every few days some rain blows through so we need to post a message that we’re closed in that case. It’s kind of a boring fruit stand but that’s ok 🙂
The data structures that we’ll use are laid out below. Note that all of the struct
objects implement the Hashable protocol, which is a requirement to use diffable data sources. Both the SectionIdentifierType
and ItemIdentifierType
must implement Hashable
so that the data source can differentiate one row or section from other.
struct Apple: Hashable {
var name: String
var coreThickness: Int
}
struct Orange: Hashable {
var name: String
var peelThickness: Int
}
struct EmptyData: Hashable {
let emptyMessage = "We're sorry! The fruit stand is closed due to inclement weather!"
let emptyImage = "cloud.bold.rain.fill"
}
So now we just need to create our view controller and an enum for each different section we might want to use. There’s also a random function to generate data or the empty data set depending on the “weather”. Approximately 33% of the time we run the playground code, we should see the “bad weather” scenario and the empty data cell instead of a list of apples and oranges.
final class DiffableTableViewController : UIViewController {
var tableView: UITableView!
enum Section: String, CaseIterable, Hashable {
case apples = "Apples"
case oranges = "Oranges"
case empty = "No Data Found"
}
override func viewDidLoad() {
super.viewDidLoad()
...<code>...
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "AppleCell")
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "OrangeCell")
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "EmptyDataCell")
...<code>...
// Just a silly method to pretend we're getting empty data every 3rd or so call (for demo purposes every 3 days or so we get rain at the fruit stand)
let badWeatherDay = Int.random(in: 0..<3)
badWeatherDay > 0 ? getData() : getEmptyData()
}
class DiffableViewDataSource: UITableViewDiffableDataSource<Section, AnyHashable> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
//Use the snapshot to evaluate the section title
return snapshot().sectionIdentifiers[section].rawValue
}
}
}
As part of the table view setup for this example, I’ve also created three different types of cells (because apples, oranges, and empty data are different). Another important item to mention is that we’ve subclassed UITableViewDiffableDataSource
. This isn’t required but is useful if you want to override any of the table or collection view data source methods. In our case, we’re using this subclass to set the title header to the raw value of the section identifier.
With what we’re trying to do here, it’s also important to note that we’re leaving the item type as AnyHashable
. This will allow us to test the item type and show the appropriate UI. If you just have one item type, you can actually specify the type of the item when the data source is defined (e.g. Apple
or Orange
instead of AnyHashable
).
Configuring the Diffable Data Source
Now that we’ve got a view and some data, let’s create the data source.
private lazy var dataSource: DiffableViewDataSource = makeDataSource()
makeDataSource
is just a function that we’ve written that will create the data source and switch between the different cell types based on the type of the item.
/// Create our diffable data source
/// - Returns: Diffable data source
private func makeDataSource() -> DiffableViewDataSource {
return DiffableViewDataSource(tableView: tableView) { tableView, indexPath, item in
if let apple = item as? Apple {
//Apple
let cell = tableView.dequeueReusableCell(withIdentifier: "AppleCell", for: indexPath)
cell.textLabel?.text = "\(apple.name), core thickness: \(apple.coreThickness)mm"
return cell
} else if let orange = item as? Orange {
//Orange
let cell = tableView.dequeueReusableCell(withIdentifier: "OrangeCell", for: indexPath)
cell.textLabel?.text = "\(orange.name), peel thickness: \(orange.peelThickness)mm"
return cell
} else if let emptyData = item as? EmptyData {
//Empty
let cell = tableView.dequeueReusableCell(withIdentifier: "EmptyDataCell", for: indexPath)
cell.textLabel?.text = emptyData.emptyMessage
return cell
} else {
fatalError("Unknown cell type")
}
}
}
This really the guts of how we are going to deal with different object types needing different heterogenous UI. We’re just testing item
variable above and acting on that object if we have one. While diffable data sources do include the indexPath
variable, the whole point of using them is to move away from counting sections and rows and managing that complex table state. While you might consider using indexPath
to achieve much the same thing as above, that introduces risk and many of the state management problems that diffable data sources solve.
Updating the Snapshot
A key part of using diffable data sources is to use the NSDiffableDataSourceSnapshot
struct to update the table or collection view when data changes (rather than trying to insert or delete table rows or sections). So let’s look at the definition of this class.
@available(iOS 13.0, tvOS 13.0, *)
public struct NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
We can see the requirement for the Hashable
protocol as well for the sections and items (once again so that items can be differentiated).
Our updateTable
function below is called whenever we have received some new data and are ready to update the diffable data source via the snapshot. In our simple playground, this just happens in viewDidLoad
but in real life, you would probably be calling an API or populating from local data at various points.
/// Update the data source snapshot
/// - Parameters:
/// - apples: Apples if any
/// - oranges: Oranges if any
private func updateTable(apples: [Apple], oranges: [Orange]) {
// Create a new snapshot on each load. Normally you might pull
// the existing snapshot and update it.
var snapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>()
defer {
dataSource.apply(snapshot)
}
// If we have no data, just show the empty view
guard !apples.isEmpty || !oranges.isEmpty else {
snapshot.appendSections([.empty])
snapshot.appendItems([EmptyData()], toSection: .empty)
return
}
// We have either apples or oranges, so update the snapshot with those
snapshot.appendSections([.apples, .oranges])
snapshot.appendItems(apples, toSection: .apples)
snapshot.appendItems(oranges, toSection: .oranges)
}
The defer
keyword ensures that the data source snapshot is applied before the function exits.
When we’ve got good weather, we’ll see a UI like this from our playground.

If we’ve got storms in the area, we’ll see this UI.

Summing Things Up
That’s pretty much all there is to it with this method of using heterogenous data and a diffable data source. There are obviously different ways to approach the problem of using different types of cells and data with these data sources. However you choose to approach the problem though, it’s advisable to avoid using the indexPath
to do so. By relying on the power of snapshots and hashable types to power your table or collection, you can reduce your risk of runtime errors and write less code at the same time.
Get the source for this blog post from Github.
Very useful info! Thank you for sharing
LikeLike