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
App Store

Lately 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 enums 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

Jokes Loading Shimmer

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

Filip Němeček profile photo

WRITTEN BY

Filip Němeček @nemecek_f@iosdev.space

iOS blogger and developer with interest in Python/Django. Want to see most recent projects? 👀

iOS blogger and developer with interest in Python/Django. Want to see most recent projects? 👀