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 Sponsored App StoreUpdate 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 enum
s 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