Using iOS Diffable Data Sources with Different Object Types

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.

UI with two different sections from a diffable data source

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

UI with empty data section from a diffable data source

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.

Published by Mark Thormann

As a software developer and architect, I enjoy using technology to craft solutions to business problems, focusing on all aspects of native iOS and Android mobile development as well as application architecture, automation. and many other areas of expertise. I'm currently working at one of the leading career-related companies in the United States, using mobile applications to help connect job seekers in the technology industry to the employment which they need.

2 thoughts on “Using iOS Diffable Data Sources with Different Object Types

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 )

Google photo

You are commenting using your Google 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

%d bloggers like this: