Implementing loading / shimmer with Diffable Data Source
Quick look on one way how to implement loading state for your collection view or table view when using diffable.
Published: Dec. 6, 2020 Sponsored See booksLately I became a huge fan of Diffable Data Sources. They have nice fresh modern API which makes building complex collection views way easier than with the old data source. One area that was a bit difficult at first was how to implement loading state. Traditionally, you would just return something like 10 items in the numberOrRows
methods and when configuring cell, based on whether you have a data, show either loading state or the actual data.
With diffable it is all about snapshots. Here a first problem arises, because diffable needs all sections and items to be Hashable
. This way it can efficiently compute what changed and animate changes when you apply snapshot.
In this example, I am going to use a screen that fetches a bunch of jokes from public API and displays them in a collection view. The source code is available in my Compositional Diffable Playground project.
First the minimal setup with type aliases:
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>
typealias Datasource = UICollectionViewDiffableDataSource<Section, Item>
Data model
Both the Sections
and Item
are enum
s which are great for these kinds of definitions.
Below is what the first naive implementation might look like:
enum Section: Hashable {
case jokes
}
enum Item: Hashable {
case loading
case joke(JokeDTO)
}
This already successfully compiles. Note that JokeDTO
has to have Hashable
implemented for this to work.
However, if you construct snapshot for loading. Maybe with a code like this:
var snapshot = Snapshot()
snapshot.appendSections([.jokes])
snapshot.appendItems([.loading, .loading, .loading])
You will get a crash. The .loading
items are not unique and data source cannot display them.
UUID to the rescue
I have tried a few different patterns so far to solve this. The start is to have UUID
as an associated value for the .loading
enum. Since each instance of UUID
is unique, the loading will work nicely now.
So here is the updated Item
enum:
enum Item: Hashable {
case loading(UUID)
case joke(JokeDTO)
}
However constructing the snapshot is tedious and not very pleasant code.
snapshot.appendItems([.loading(UUID()), .loading(UUID()), .loading(UUID())])
And now imagine we want to have 10 loading placeholders. Another pattern I tried vas to define something like randomLoading
as a computed property on the Item
enum but that still meant repeating the items.
We could use special Array
init which automatically repeats any value for us. Like this:
Array(repeating: Item.loading(UUID()), count: 10)
This is nice code, right? The problem is that it will not work. It will create single .loading
item and repeat that. Even if you try to surround the expression with closure and immediately invoke it, it will still repeat the same item.
Extension for cleaner usage
So for now (until I find even cleaner way) I created an extension
on Array
that repeats an expression:
extension Array {
init(repeatingExpression expression: @autoclosure (() -> Element), count: Int) {
var temp = [Element]()
for _ in 0..<count {
temp.append(expression())
}
self = temp
}
}
And the usage:
Array(repeatingExpression: Item.loading(UUID()), count: 8)
This will correctly produce eight (in this case) .loading
items with unique UUID
values.
And we can further abstract this into computed property on Item
:
enum Item: Hashable {
case loading(UUID)
case joke(JokeDTO)
static var loadingItems: [Item] {
return Array(repeatingExpression: Item.loading(UUID()), count: 8)
}
}
And create our loading snapshot:
var snapshot = Snapshot()
snapshot.appendSections([.jokes])
snapshot.appendItems(Item.loadingItems)
Finished example
And this is it. Thanks for reading. I don't think this is the best approach, but I believe it is clean enough and works pretty well. If you solved this issue differently, I would love to hear how.
Uses: Xcode 12 & Swift 5.3