Tips and practices for setting up Diffable Data Sources

In this post I would like to show a few approaches that make working with Diffable cleaner and better.

Published: Jan. 11, 2021
App Store

Update 26. May 2021: Previous version of the article used incorrect code that would create retain cycles. I am truly sorry. I had no idea that passing methods could cause them.

Typealises

typealias is simple and powerful Swift feature. It allows you to basically give existing type another name. This can be used to add more context for example (TimeInterval is alias for Double) or shorten specific types that have generic parameters. This second use case is great for Diffable.

In views controllers where I work with Diffable, I tend to have these two aliases:

typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>
typealias Datasource = UICollectionViewDiffableDataSource<Section, Item>

You then have just single place where you need to change the generic arguments and for example creating a new snapshot is just this:

var snapshot = Snapshot()

Also the datasource property becomes just this:

var datasource: Datasource!

Methods instead of closures

When you create an instance of diffable data source the last parameter is a closure used to return the correct cells. This can quickly get very hard to read, especially when you have more than one cell.

However, you don't have to create this closure inline, you can define method with this signature:

func cell(collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell?

The Item is your model.

And next use this method when creating the datasource. Together with the typealias, creating new datasource becomes cleaner, but we cannot assign the method directly, since that would create retain cycle and therefore memory leak:

datasource = Datasource(collectionView: collectionView, cellProvider: { [unowned self] collectionView, indexPath, item in
    return self.cell(collectionView: collectionView, indexPath: indexPath, item: item)
})

This is one of few places I am comfortable with unowned self since lifecycles of these objects are the same.

You can use this same approach to setup code for supplementary views (headers, footers and other extra views that are available).

First create method with this signature:

func supplementary(collectionView: UICollectionView, kind: String, indexPath: IndexPath) -> UICollectionReusableView?

And then assign it:

datasource.supplementaryViewProvider = { [unowned self] collectionView, kind, indexPath in
    return self.supplementary(collectionView: collectionView, kind: kind, indexPath: indexPath)
}

Once again we need to prevent retain cycle.

Enums for sections and items

While diffable data source section and item can be any type that conforms to Hashable. I would strongly recommend you use enums for it. You can then have heterogenous collection views with ease. Each different thing gets its own enum case.

You need to make sure that each item is unique, you get a crash if you add duplicate items. If your type does not have any natural id that would be suitable for Hashable protocol, you can use UUID type which is guaranteed to be unique and it is also memory efficient.

For example this is a section definition from my example project that shows random jokes from the Internet and also saved ones (if user saved any).

enum Section: Hashable {
    case favoriteJokes(count: Int)
    case jokes
}

The count is used to set title for favorite section header.

And here are the items:

enum Item: Hashable {
    case loading(UUID)
    case joke(JokeDTO)
    case favorite(Joke)
}

JokeDTO has id property so I implemented Hashable like this:

extension JokeDTO: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }

    static func ==(lhs: JokeDTO, rhs: JokeDTO) -> Bool {
        return lhs.id == rhs.id
    }
}

And Joke is Core Data object so there is no need for additional implementation. The loading item needs to have UUID because I want to display 10 loading items and without this associated value the items wouldn't be unique. I have written more about loading states with diffable data sources here.

Enums are also great because you can cleanly use them with switch and once you add a new case the compiler will tell you where you need to make modifications. This assumes you are not using default case.

Enums with associated values

Another tip and clean pattern is to always add all the data you need to your snapshots. If your footer view needs some data, make it associated value for the section enum. (This will also make reloading these views automatic.)

This also applies to the items. Basically you should always use only the data in the item parameter when configuring cells. This makes it much simpler and you don't have to use the provided indexPath to reach out to pull additional data with the index.

Single method to create snapshot

Snapshots are the core of diffable data sources. While previously we needed to provide number of sections and number of rows for each section, now all we need is to construct the snapshot.

This is awesome because you don't have to have complicated logic if you are showing various kind of data that depend on a few other factors. For example if you wanted to show data from multiple sources (database, network) together with maybe warning about missing location permissions and also some loading state items..

With diffable you just construct snapshot based on current state and call the apply method. There is very little room for serious errors.

In my projects I have single snapshot() method which is the only place where I am creating snapshots. If anything related to the data changes, I just update the datasource with the new snapshot.

Uses: Xcode 12 & Swift 5.3

Bluesky logo

Follow on Bluesky to not miss new posts

Filip Němeček profile photo

WRITTEN BY

Filip Němeček Mastodon

iOS blogger and developer with interest in Python/Django.

iOS blogger and developer with interest in Python/Django.