Can UITableViewDiffableDataSource detect an item changed?
Asked Answered
B

3

11

(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?

  • An item has several properties. One property (let's call it property A) is shown in UI but is not used for generating hash.
  • The item exists in both old and new snapshots, but its property A changes. So its UI needs to be updated.

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.

Bilestone answered 7/4, 2020 at 5:27 Comment(0)
B
11

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.

Bilestone answered 8/4, 2020 at 1:27 Comment(21)
“But it's so fundamental that I can't understand why it isn't mentioned explicitly anywhere.” It’s mentioned explicitly everywhere. Hashability is how diffable data source works. Hashable implies Equatable implies ==.Induna
@Induna It's mentioned everywhere that Hashable requires Equatable in swift. But it NOT mentioned anywhere how == is used in diffable data source. In the example code in the WWDC 2019 session 220 video, the definition of == func is essentially same as hash(into:) func. In my option, that is very misleading. Unless I completely misunderstand it, diffable data source actually deals with the situation very well where two items has same hash value but are not equal. I believe this is part of diffable data source's design and the speakers really should have metioned it explicitly.Bilestone
But how does a Set work? How do the keys of a Dictionary work? They “deal with the situation very well where two items has same hash value but are not equal”. That is what hashability is. It permits rapid discovery of whether an equal item is already present in a large collection. That doesn’t need further mention; it is the essence of Hashable.Induna
I’ll put it another way. All the section identifiers or row identifiers of a diffable data source, just like all the elements of a Set, must be unique. Unique means none of them are equal to any other. That’s ==. That is the essence of how the data source identifies an object. That is why these are called identifiers! If an object was in row 1 and an object equal to it is in row 3 now, they must be the same object. — Hashability is merely the tool that allows that determination to be made more or less instantly, without having to examine every other value every time.Induna
@Induna I agree what you said about Set or Dictionary, in which hash is just a tool not an essential part of the data. However, I'm not sure if your are right about hash's use in diffiable data source. I think you considered the question based on the assumption that item type was a value type. But what if it's an NSManagedObject subclass? If the object's property or relationship changed, but user passed the its reference in both old and new snapshot, do you think if diffable data source can detect change and how?Bilestone
That's what confused me yesterday and I worked out a theory about how diffable data source worked based on my experiments and solved that issue. In my understanding, hash is used as item identifier, and == is used for detecting changes. I see you think it in an opposite way (though you didn't say that hash is for detecting change). I hope you're right, because that might be more reasonable if it worked. But I'm not sure and it seems hard to design experiments to prove which understanding is correct.Bilestone
Okay, so you are warned very loudly not to use a mutable object in a Set or as a Dictionary key, because the collection will break. I venture to say that the same thing is true in a diffable data source; entries must not mutate in place. But if you make a change and then apply a new snapshot, all will be well.Induna
Yes, I'm talking about an item changes in new snapshot. I mean the item still exists but its "value" (the data to be shown in its row, which can be value of the item or referenced by it) changes. How do you think diffable data source detect it? This is the information I think is missing.I think the theory in my answer explained it.Bilestone
Do you have a solution if the object in the data source comes from a NSFetchedResultController and thus are NSManagedObject subclass?Posit
@Bilestone thanks for the question and for your answer. I want to ask a missing point. In your original question, your aim was handling of the reloadCell functionality for a cell which is still available in the data but with some other changes. It's ok the modify hasher/== functions to show any change to table but then tableview reacts as removing of the old version of the cell and inserting the new version according to its animations. So, could you found any solution to make tableview reacting as a reloading the updated cell instead of replacing it with the new version of it?Dainedainty
@Dainedainty I'm not sure if I understand your question. My purpose is NOT to call reloadData() or reloadRows() because they are old API and diffable data source don't need them. So the key point is how data source detects data changes, for which I gave explanation in my answer. Regarding how to achieve the UI effect you want, you need to prepare the data properly. That is, you should keep the data item's hash value unchanged and change its other parts. Hope that helps.Bilestone
Also, I wrote the question when I had no idea of how diffable data source worked. I was very confused at that time. I'd suggest you skipped the question and just read my answer (I'm quite sure the answer is correct).Bilestone
@Bilestone Was your final solution to only include the id property in the hash function but include both the id and property A in the == operator? Then using reloadItems with the array of items where the id is the same as those already in the snapshot but the value of property A are different?Epilepsy
@Epilepsy You don't need to overload == operator. You just define your struct and use the default == implementation. I do define a custom hasher() method. Whether that it's necessary or not depends on the context, but I find I do it often. As explained in other comments, I don't call reloadItems at all. That's the point of using diffable data source. I'd suggest you do experiments to try different approach and see the results yourself. And you should be able to figure out what I meant.Bilestone
On a second thought, it may help to define a custom == operator (though I didn't do that in my app).Bilestone
@Bilestone can you please share some examples, as I tired according to your answer but it didn't work. I have one struct whose values change on didSelect. Any help would be appreciated.Deflocculate
@PoojaGupta I don't have one which I can show you online (I thought about writing a blog post about this, but never did it). In your code, did you create a new snapshot in didSelect after you changed your data? If you didn't, you should. Otherwise, i can't think out that the issue might be. Can you post your example code online? I can help to take a look.Bilestone
@Bilestone I did try creating a new snapShot in didSelect but it crashes whenever array's object changes. I tried using reload but it results in the same crash. It would be great if you can write a blog post as many of us are facing the same issue which Combine. Thanks in advance.Deflocculate
Your descriptions are correct. There is one more problem. If you use the hash to identify the object and == to test if some values changed, diffable data source deletes and inserts the row. If you want diffable data source to just update the row, you have to set reloadItems on the new snapshot. Edit: I think it's a bug in NSFetchedResultsController. NSFetchedResultsController should user reloadItems but it doesn't. Here is es solution for manually generate the reloadItems. https://mcmap.net/q/846440/-how-to-get-a-diffable-snapshot-from-an-nsfetchresultscontroller-in-ios-13Rhodonite
I believe such understanding "Diffable data source uses item hash to identify item". As, 2 items with different identify can end up with same hash value. In fact, I think iOS's Diffable Data Source does have a serious flaw, especially when an item has content changed and position changed at the same time. The flaw is more obvious, when we cross check with Android on how they implement Diffable concept - developer.apple.com/forums/thread/653647Punkah
@rayx, diffable data source checks if the item changes by doing an "==" operation with its old and new values. I wrote a structure just as developer.apple.com/documentation/uikit/views_and_controls/…. only ID is itemID. but when I changes any property value, and I reloadItems with itemID, I can see the effect. I am still confused about your theory and Apple. :(Calctufa
M
3

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.

Mainsail answered 9/11, 2022 at 6:2 Comment(2)
Thanks for the pointer. The article wasn't available when I posted my question (time flies!). I think it confirms the points explained in my own answer: a) diffable data source uses item hash value as its identity, and b) it detects item change by comparing its value. The first point is the most important one (it's the foundation of the second point) and it wasn't mentioned anywhere on the net when I posted my question and answer.Bilestone
Personally I think using hash value as item's identity (silently) is a confusing design decision (is this approach used anywhere else? I can't think of one). A much better approach is to let SectionIdentifierType and ItemIdentifierType to conform Identifiable protocol instead. That's the approach widely used in SwiftUI.Bilestone
H
1

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.

Heteroecious answered 7/4, 2020 at 7:21 Comment(6)
Hi @@AndreasOetjen Thanks for your answer and sorry for the confusion (they are two separate examples. I should have used different names). I'm afraid I probably didn't make it clear what I was confused about. Let me put it this way. In the old UITableViewDataSource API, if one changes an item in data source, he can call UITableView.reloadRows(). What's the equivalent way in the diffable data source API to do it? Does the new API support the concept that the value of an item with a specific identifier changes? (I can't see this from the new API).Bilestone
Oh, wait...perhaps the diffable data source doesn't really care about value of item, right? It seems that it only cares about item order change and that's the reason why it needs item identifier. As for the item's value, it's only used in the user supplied code in cellProvider closure. In my app when user edit one item in table view, it may change another item in the table view. The issue is that while that another item was changed in data layer, its new value wasn't updated in the UI layer. Seems that it might be caused by other issue other than diffable data source?Bilestone
You typically do not call reloadData, but work with NSDiffableDataSourceSnapshot and dataSource.apply. The diffable data source cares about section and row changes, insertions and deletes. This might be worthful reading: wwdcbysundell.com/2019/diffable-data-sources-first-look and medium.com/@alfianlosari/…Heteroecious
Thanks for the pointer. I know how to use it. I have just updated my quesiton to make it more clear. Thanks.Bilestone
Well, if the identity does not change (hence no add/move/delete), you should be able to work with reloading the cells at the given index paths (reloadRows(at:with:)), so you don't need to add the notes to the hash algorithm.Heteroecious
But doesn't that defeat the purpose of diffable data source? (It's actually a bit difficult to determine which items are changed as side effect in my app). Also I just tried the approach described in my question (adding 'property A' to hash) but it didn't work. I checked the hash value, it was diffent in two snapshot but its UI was not updated (I'm not using the example struct. The one in my app is an enum containing NSManagedObject as its associated value). I'm still trying to figure out what went wrong. Sigh. I probably have to resort to reloadData() if I can't identify the root cause.Bilestone

© 2022 - 2024 — McMap. All rights reserved.