Fixing wrong animations with Diffable Data Source

The most common causes and how to fix it.

Published: Jan. 11, 2022
See books

The magic of Diffable Data Source, either for UICollectionView (with UICollectionViewDiffableDataSource) or UITableView (with UITableViewDiffableDataSource) is that it does a lot of work for us. But sometimes, it doesn't quite do what we want, and it is hard to pinpoint where the problem lies.

In this post, we will look at the causes for incorrect animations - at least what I found using these APIs for the past two years.

By incorrect animations, I mean that instead of inserting one new item or removing it, Diffable will refresh the entire view, which isn't a pleasant user experience. Or you might sometimes see other glitches.

Verify your Hashable implementation first

Whenever you are using custom types for Diffable Data Source, you must implement the Hashable protocol. At the minimum to conform your struct or class to it.

Why? Because the "magic" of Diffable is based on computing and comparing hashes of items in your data source to see what is new, unchanged, or removed quickly. Similarly, for sections.

Since a lot of existing types conform to Hashable Swift can "synthesize" the implementation for your types automatically.

Synthesized Hashable means it will take all the properties of your type and combine their hashes.

In this case, I found that while UIImage - which you may frequently use for various thumbnails or icons - conforms to Hashable, it does not behave correctly. Meaning the same object won't appear as same to Diffable, causing the animations to break.

The most straightforward fix is to implement Hashable yourself and ignore the UIImage property.

How to implement Hashable for Diffable data source

The general rule is to hash all the properties that change and their values are reflected in your UI. Otherwise, you might change a property and cannot see the difference because nothing has changed from the point of Diffable.

Here is an example to make it more concrete:

struct Reminder: Hashable {
    let id: UUID
    let task: String
    let formattedTime: String
    let isFavorite: Bool

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(isFavorite)
    }
}

In this simple example, each Reminder is created with unique id and since user can toggle its favorite status, both of these are reflected in the hash. If we didn't have the hasher.combine(isFavorite) line, then toggling this property would not refresh the cell for this particular cell, and indeed it would do no refresh at all.

In this example, the other properties are constants, so we don't have to worry about hashing them.

Note that the order in which you call combine matters.

Core Data + Diffable

Another issue I frequently had was with Core Data objects being used as items in Diffable Data Source. These somewhat work out of the box but are not always reliable.

Creating a simple wrapper struct for the Core Data object and implementing Hashable works pretty well.

Continuing the reminder example, it could look like this:

struct Diffable: ReminderProtocol, Hashable {
    let id: UUID
    let task: String
    let formattedTime: String
    let isFavorite: Bool
    let reminder: Reminder

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(isFavorite)
    }
}

The Diffable struct is nested inside Reminder class which is the NSManagedObject subclass. So external use is Reminder.Diffable. And definition for Diffable looks like this for the item:

enum Item: Hashable {
    case reminder(Reminder.Diffable)
      // other cases omitted for clarity
} 

Forcing refresh each time

Another technique may be helpful in particular circumstances. If you have a cell you want to refresh each time you apply a new snapshot, you can add UUID to your model or just the enum case. When you create snapshot, instantiate a new UUID each time, which will force a refresh.

For the above Item example, it could look like this:

enum Item: Hashable {
    case reminder(Reminder.Diffable, UUID)
      // other cases omitted for clarity
} 
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.