diffable data source can't identify items?

i have a list like this:

id: 1, image: image1, title: title1, badge: 0
id: 2, image: image2, title: title2, badge: 0
id: 3, image: image3, title: title3, badge: 0
...

is my understanding correct that in order to do a smooth "expected" animation when I want to change both the badge of the item and its order i have to manually split this "big" update into two smaller updates (first change then move, or vice versa)?

this is somewhat surprising, i would expect a diffable implementation to have a notion of "identity" (in the example above it's "id") and calculate the differences based on that identity plus ite equivalence check rather than just based on the hash/equality check for the whole item.

Replies

example of behaviour that doesn't take identity into account (remove spaces): shorturl . at / ltNQ5
corrected version that takes identity into account (remove spaces): shorturl . at / bovzT

Diffable's notion of identity is based on the isEqual: implementation of the object. You have two options for your identity: for more complex objects the easiest way is to just put the ID in to your diffable data source instance. So your sections would only contain the IDs of the items shown in the cells. In your case this ID seems to be a number, so you could just put NSNumber instances in diffable data source.
The second option is to implement isEqual: (and hash) on your objects (or a separate wrapper object just for your data source) and use the objects directly. This is useful if you have multiple things that together form the identity of an object as you can combine multiple different properties of that object.

Diffable data source uses the identity of an object to determine whether an object moved or got inserted or deleted from the data source. In your case it seems that the identity is just the ID of the object while an update of a badge is rather a change in how this object is represented.

So I would advice to use the first approach for identity and once your badge changes, you can either apply a snapshot that explicitly reloads the particular identifier (or moves it around as you mentioned) or if you only require a change that does not alter the height of the cell, you could also just grab the cell directly from the collection view via cellForItemAtIndexPath: and then update the cell directly. The collection view or table view only needs to know about it if the update somehow changes the layout, e.g. by requiring a different height for the cell.
it would be simpler on the usage side is diffable data source relied on explicit item identities (like SwiftUI does). then it would be able to figure out that the two different (as per isEqual) items are indeed the same item (same ID) with a changed content and thus issue the relevant "reload" of that snapshot item (*) without me having to do that manually/explicitly.

(*) this is more obvious when the item in question is also moved, in which case the resulting animation shall be move + change, rather than "teleport" as in the first video above.

in regards to changing cell heights: i figured that calling beginUpdates + endUpdates is enough for UITableView to notice the change in height of a particular cell i am modifying without reloading that cell. is that correct approach?
If you want this to behave different it would be great if you could file a feedback ticket for this. Given that this is already API and changing this would break existing clients, it is rather unlikely to have this change with the existing diffable data source. But it is always good to have this kind of feedback when planning new APIs.

Calling begin/end updates or just an empty performBatchUpdates: block will also invalidate self sizing informations / invalidate the layout on a UICollectionView, so this will work as well, yes.
I believe this enhancement can be done in a pure additive manner, so the old clients would still work as they are now and the new clients who opt-in into that feature (e.g. by marking the model items identifiable) would benefit from it. see the round numbered FB7973000 which has an example of a possible implementation.

or if you only require a change that does not alter the height of the cell, you could also just grab the cell directly from the collection view via cellForItemAtIndexPath: and then update the cell directly. The collection view or table view only needs to know about it if the update somehow changes the layout, e.g. by requiring a different height for the cell.

can you elaborate on this a little bit further: what are the rules here exactly?

examples:

1) UICollectionView with horizontal flow layout and one item per row. the width of one of the items is increased by one pixel (e.g. because of a changed done to it's width constraint). i am not calling reload on this item, nor do i call beginBatchUpdates with an empty block. does UICollectionView layout system know about about this change and will everything be alright (*)?

2) UICollectionView with horizontal flow layout and two items per row. the width of the left item of one of the rows is increased by one pixel (e.g. because of a changed done to it's width constraint). The neighbour item to the right shall move one pixel to the right to maintain the minimal item spacing. i am not calling reload on the item whose width is changing, nor do i call beginBatchUpdates with an empty block. does UICollectionView layout system know about these geometric changes to both of the items and will everything be alright (*)?

3) UICollectionView with horizontal flow layout and two items per row. the width of the left item of one of the rows is increased by a few pixels (e.g. because of a changed done to it's width constraint). The subsequent item shall overflow to the next line as it can't longer fit on the current line. i am not calling reload on this item, nor do i call beginBatchUpdates with an empty block. does UICollectionView layout system know about this change and will everything be alright (*)?

4) UICollectionView with vertical flow layout and one item per column. now this is the height changes of the items are "benign", and the width changes are troublesome, is it not?

5) UICollectionView with a custom layout. The width of one item is changed by one pixel by autolayout constraint system. does UICollection view take a note and will everything be alright (*)?

(*) so that the layoutAttributesForElements(in rect: CGRect) returns correct and up to date items and layoutAttributesForItem(at indexPath: IndexPath) returns correct up to date rects for the items.

i did some testing and here is what i found:

a) when i use autosizing cells with collection view i can't just change the constraint of a particular cell (e.g. increase cell's width by one pixel) and have collection view to notice the change - nothing changes, nor visually on the screen nor via testing what layoutAttributesForItem returns.

b) in order to change my cell width (in my case via the cell's width constraint) in addition to changing the constraint i have to call performBatchUpdates on the table.

c) it can be either an "empty" performBatchUpdates done after changing the constraint, or i can put constraint changing inside performBatchUpdates's block - the end result is the same in both cases.

d) there is some internal constraints at play (one is named "UIView-Encapsulated-Layout-Width")

e) when i change my constraint i can see a warning in the console for "Unable to simultaneously satisfy constraints" followed by "Will attempt to recover by breaking constraint" for MY constraint. despite of this - at the end of the day my constraint wins as the end result is a cell with changed width (and other cells moved accordingly, see below for more *).

f) i wasn't able getting rid of that warning other than by reducing my constraint's priority to less than required. the hypothesis is that with those two constraints at play the system's one is required and mine is not required (e.g. defaultHigh) so the console warning is not shown; but by the end of performBatchUpdates the system finally notices the change and update's it's constraint to match the new reality, so it works (*)

(*) it all kind of works... but see how weird it actually works: http shorturl.at/glqMY
here i am increasing the very first cell width by one point along with calling performBatchUpdates (as otherwise the change is ignored).
  • notice how the bottom cell suddenly moves (why?!).

  • also note how the first cell bounces

  • and once the first line overflows notice how the entire list redraws weirdly

obviously this behaviour is unacceptable and i need to find a better way of changing cells. the three other obvious candidates are "reloadItems at index paths", "diffable data source apply" (on iOS 13+), and ditching autosizing cells and instead of this changing the attributes of the item in question via invalidating the item's attributes (how?) and returning the new attributes via "layoutAttributesForItem".

As long as the size of a cell changes, you should always go through the update path. Otherwise UICollectionView will have no knowledge about this change.

However there are plenty of changes where this is not the case. Imagine a cell with a fixed 44x44pt image and you want to swap out that image. Or a cell that always covers the full width of the collection view and has a single line label as a title where you want to update the label.

Also more complex examples like in a social network app where you might have a title and next to it a '2 seconds ago' label. If the title is single line and truncates to make room for that time label, there is no way that updating that label would change the size of the cell. So you can safely just update that time label directly. AutoLayout inside the cell will update everything, but the cell's size doesn't change.

Especially if you update things often, like in the time label, where you might want to update the label up to once per second, it is usually faster and simpler to just fetch the cell instance and updating the label directly, as it doesn't require the collection view to invalidate its layout and then check for new cell sizes.
perhaps UITableView can tolerate width (but not height) changes without the need of reloading cells, and UICollectionView is less forgiving.

in practice the situation with "2 seconds ago" labels is further complicated by the fact that such labels "age" and eventually wrap into "yesterday" and if you use section headers for different days that would mean that the cell shall now be in a different section (yesterday vs today) at which point you have to do delete/insert or reloadData/apply. or you just sort/filter by date, in which case updating the cell contents is obviously not enough.
The way of iOS to implement Diffable Data Source obviously has a major flaw.

It is not able to detect, when the content changed and position moved happen at the same time on an item.

If we look how Android implements their version of "Diffable Data Source" - DiffUtil. It comes with 2 functions

Code Block
areContentsTheSame(int oldItemPosition, int newItemPosition) - Called by the DiffUtil when it wants to check whether two items have the same data.


and

Code Block
areItemsTheSame(int oldItemPosition, int newItemPosition) - Called by the DiffUtil to decide whether two object represent the same Item.


As pointed out by yetanotherme , besides the ability to detect content change, iOS's Diffable Data Source need a way to distinguish whether 2 items are having same identify.

Same analogy is... Yesterday my hair color is black, today my hair color is white (Content change. In Android, this is detected using areContentsTheSame)

But, Yesterday me and today me are still referring to the same me (Same identify. In Android, this is detected using areItemsTheSame)


The points made by the engineer from Apple in this thread are inconsistent with this doc.

Above:

Diffable's notion of identity is based on the isEqual: implementation of the object.

In the doc

A diffable data source stores a list of section and item identifiers, which represents the identity of each section and item contained in a collection view. These identifiers are stable, meaning they don’t change.

Because identifiers are hashable and equatable, a diffable data source can determine the differences between its current snapshot and another snapshot. Then it can insert, delete, and move sections and items within a collection view for you based on those differences, eliminating the need for custom code that performs batch updates.

Apple's docs and Apple's engineers don't seem to agree on the behaviour here. Based on the info in the doc, a difference in the fields used for implementing the hash and equality functions should be sufficient to determine that a position change and a content change has happened simultaneously, but that does not appear to be the case in practice.