Collection views and table views need to know what data to display, where to display it and how to move it. Data sources give our collection views and table views of this data. Diffable data sources provide a more simple and more stateless way to provide our data.

What is it?

A diffable data source is a subclass of either UITableViewDataSource or UICollectionViewDataSource. It is generic over two Hashable types. The types are SectionIdentifierType and ItemIdentifierType.

SectionIdentifierType defines the sections to show in your table view or collection view. Many single section implementations simply use an enum.

enum Section {
    case .main
}

ItemIdentifierType defines the data or models that support individual cells. A basic cell displaying an image and label could be modelled with a Struct.

struct CellModel {
    let title: String
    let imageIdentifier: String
}

The generic constraints of the diffable data source tell the data source which sections will exist and what type of models will provide data to cells.

The diffable data source also needs to now what cells to show. The initialiser of UITableViewDiffableDataSource shows how this is achieved.

public typealias CellProvider = (_ tableView: UITableView, 
                                 _ indexPath: IndexPath, 
                                 _ itemIdentifier: ItemIdentifierType) 
                                 -> UITableViewCell?

public init(tableView: UITableView, 
            cellProvider: @escaping CellProvider)

The initialiser requires a UITableView and a CellProvider, which is a function. The function takes a table view, index path and item identifier and returns an optional UITableViewCell. The CellProvider function is what tells the data source what cells to display. Typically the function involves dequeueing reusable cells, configuring them and returning them.

With this in mind creating a UITableViewDiffableDataSource for a UIViewController might look like the following.

func makeDataSource(tableView: UITableView) -> UITableViewDiffableDataSource<Section, CellModel> {
    UITableViewDiffableDataSource(tableView: tableView, cellProvider: cellProvider)
}
    
func cellProvider(tableView: UITableView, 
                  indexPath: IndexPath, 
				  model: CellModel) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: Cell.reuseIdentifier, for: indexPath) as! Cell
    cell.textLabel.text = model.title
    cell.imageView.image = UIImage(named: model.imageIdentifier)
    return cell
}

The data source cannot be initialised until a table view has been initialisd because the data source requires a table view. Because of this a lazy variable would be an appriopriate property on the view controller.

lazy var dataSource = makeDataSource(tableView: tableView)

Notice the injection of a UITableView is unlike the older UITableViewDataSource. Previously data sources were assigned as variables of a table view. The table view would request information about it’s data from the data source. Think cellForRowAt and numberOfRowsInSection. This often required the data source to observe, store and often share state with other classes like a parent view controller or a supporting view model.

Diffable data sources are injected with a UITableView or UICollectionView to reduce the exposed need for shared state as they can reference the views directly. This makes the diffable data source API much more concise and easier to reason about.

Snapshots

A core concept behind diffable data sources are snapshots. A NSDiffableDataSourceSnapshot is a snapshot of the state of the data that you want to render. Updating your table view or collection view is simply an act of configuring a Snapshot and asking the data source to apply it.

Like the diffable data sources, a Snapshot is also generic over SectionIdentifierType and ItemIdentifierType. These types should match the types of the diffable data source.

The Snapshot API provides a number of methods to configure its state. The method inputs depend on provided section and identifier types.

// NSDiffableDataSourceSnapshot<Section, CellModel> provides...

func appendSections(_ identifiers: [Section])
func appendItems(_ identifiers: [CellModel], toSection: Section?)
func insertItems(_ identifiers: [CellModel], afterItem: CellModel)
func moveItem(_ identifier: CellModel, afterItem: CellModel)
func deleteItems(_ identifiers: [CellModel])

Once sections and models are added to the snapshot, calling apply on the diffable data source tells it to update the view.

typealias Snapshot = NSDiffableDataSourceSnapshot<Section, CellModel>
var snapshot = Snapshot()
snapshot.appendSections([.main])
snapshot.appendItems(models, toSection: section)
dataSource.apply(snapshot, animatingDifferences: true)

After calling apply the diffable data source leverages the fact its section and item types are Hashable to compute the difference between its current snapshot and the one it is about to apply. It only applies the necessary changes. If no models changed state or position it would do no work. If two models switched places it would only update the two relevant cells.

Selecting

Selecting elements in a diffable data source driven table view can be done through the didSelectRowAt method of the table view delegate. Diffable data sources allow us to get items and sections from index paths and ints by using the itemIdentifier(for: IndexPath) and sectionIdentifier(for: Int) methods.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
        // do something with the item
    }

Benefits

  • DiffableDataSources give us simple and concise code to back fast and effective table views and collection views with data.
  • The apply(snapshot) API gives us a clear entry point into updating the view state, without having to retain state in multiple places.
  • Supporting view models can think strictly in output, where the output of any view model is the input to apply(snapshot).

Remember

  • Be Hashable and Equatable. If you do any custom hashing or equating, make sure you do it right so that model states are always different. If you don’t you’ll get unexpected display updates.