rayx
rayx

Reputation: 1710

Can UITableViewDiffableDataSource detect an item changed?

(The question was rewritten after discussing with @AndreasOetjen below. Thanks for his comments.)

I ran into an issue with using UITableView with diffable data source. In my app when user modifies an item, it may change another item which is shown in the same table view. The issue is that, after I created and applied a new snapshot containing both items' new values, the indirectly changed item's UI wasn't updated.

At first I thought diffable data source was able to detect an item's value change in different snapshot. For example, it might work this way: if it found both snapshots contains the same item (that is, items in both snapshots have same hash value), it compared their values and updated that item's row in table view if value changed. However, I later realized it perhaps didn't work that way because diffable data source doesn't define any API to get and compare item value (my original thought was it used description computed property and == operation, but now I believe it's not true).

So my current understanding is diffable data source uses item's hash for detecting item order change (i.e., new item inserted, an old item still existed, etc.), instead of item value change (i.e., an old item still existed but its value changed). If that understanding is correct, it then begs this question: how to use diffable data source to implement the following scenario?

In the old UITableView API, this can be implemented by calling reloadRows() or reloadData(). But how to do it using diffable data source?

UPDATE:

After spending time doing experiments and solving the issue, I believe the understanding in above question was incorrect. Please see my answer below. I believe that explains how diffable data source works. I hope it helps to others who'll have the same confusion. I'd be glad to be proved wrong. Really. So please leave your answer if you think differently.

Upvotes: 12

Views: 11919

Answers (3)

Kyle Xie
Kyle Xie

Reputation: 415

I have the same problem. And after some research, I think Hashable is not the way to handle the updating feature. You can see it from the document here: https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/updating_collection_views_using_diffable_data_sources.

It has 2 ways to load the diffable data source: Load the Diffable Data Source with Identifiers and Populate Snapshots with Lightweight Data Structures .

While the first one is recommended by Apple. In which, we use snapshot.reconfigureItems to update the existing items.

    struct Recipe: Identifiable, Codable {
        var id: Int
        var title: String
        
        // and many other properties
        xxxxx
    }

    // Get the diffable data source's current snapshot.
    var snapshot = recipeListDataSource.snapshot()
    // Update the recipe's data displayed in the collection view.
    snapshot.reconfigureItems([recipeId])
    recipeListDataSource.apply(snapshot, animatingDifferences: true). 

The point is instead of using Recipe in the snapshot, we're using Recipe.ID, the type is NSDiffableDataSourceSnapshot<RecipeListSection, Recipe.ID>.

For the second way, which we're all using, putting the Hashable models in the snapshot, here is what Apple says about it:

The downside of this approach is that the diffable data source can no longer track identity. Any time an existing item changes, the diffable data source sees the change as a delete of the old item and an insert of a new item. As a result, the collection view loses important state tied to the item. For instance, a selected item becomes unselected when any property of the item changes because, from the diffable data source’s perspective, the app deleted the item and added a new one to take its place.

Also, if animatingDifferences is true when applying the snapshot, every change requires the process of animating out the old cell and animating in a new cell, which can be detrimental to performance and cause loss of UI state, including animations, within the cell.

Additionally, this strategy precludes using the reconfigureItems(:) or reloadItems(:) methods when populating a snapshot with data structures, because those methods require the use of proper identifiers. The only mechanism to update the data for existing items is to apply a new snapshot containing the new data structures, which causes the diffable data source to perform a delete and an insert for each changed item.

Storing data structures directly into diffable data sources and snapshots isn’t a robust solution for many real-world use cases because the data source loses the ability to track identity. Only use this approach for simple use cases in which items don’t change, like the sidebar items in this sample, or when the identity of an item isn’t important. For all other use cases, or when in doubt as to which approach to use, populate diffable data sources and snapshots with proper identifiers.

Upvotes: 5

rayx
rayx

Reputation: 1710

After almost one day's clueless experiments, I believe I figured out how diffable data source worked and solved my issue based on that understanding (it turned out my original thought was almost correct).

Diffable data source uses item hash to identify item. For the same item that exists in both old and new snapshots, diffable data source checks if the item changes by doing an "==" operation with its old and new values.

Once figured out, it looks like quite obvious and simple approach. But it's so fundamental that I can't understand why it isn't mentioned explicitly anywhere.

So, to answer my original question, yes, diffable data source can detect item value change. That said, it becomes tricky when item value is of reference type and/or the text shown in row is, say, properties of objects referenced by that object (e.g., relationship in Core Data), etc.

Another note. Whether using entire item struct or just part of it to generate item hash doesn't matter, as long as it identifies the item. I prefer to using only the essential part of the item which really identifies it.

Upvotes: 12

Andreas Oetjen
Andreas Oetjen

Reputation: 10209

I'm a little confused about your last sentence: You write my item is an enum with associated values of reference type, but in your example above you use struct Book, which is a value type. Regardless of that, the following has to be kept in mind for any case:

Hashing is all about "object" identity. It's just a kind of shortcut to improve identity comparisons, folding etc.

If you provide a custom hash implementation, two objects a and b must behave in a way that a == b implies that also hash(a) == hash(b) (The other way round is almost always also true, but there may be collisions - esp. with weak hash algorithms - when this is not the case).

So if you only hash the title and author, then you have to implement the comparison operator in a way that it also only compares title and author. Then, if notes change, neither the data source nor any body will not detect identity changes at all.

UITableViewDiffableDataSource is a means to facilitate the synchronization of insert/delete statements between view and data source. If you ever got this

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the table view after the update (3) must be equal to the number of sections contained in the table view before the update (3), plus or minus the number of sections inserted or deleted (0 inserted, 2 deleted).'

then a diffable data source is your friend.

I hope this helps a little.

Upvotes: 1

Related Questions