Fixing wrong animations with Diffable Data Source
The most common causes and how to fix it.
Published: Jan. 11, 2022 Sponsored I want a Press KitThe 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
}