How can I reload items without removing and inserting with UITableViewDiffableDataSource?
Asked Answered
M

2

16

I'm implementing a search screen in my app using UITableViewDiffableDataSource. Each cell represents a search hit and highlights the search match in the cell title, kind of like Xcode's Open Quickly window highlights portions of its result items. As text is typed into the search field, I update the results list. Results move up and down in the list as their relevance changes.

The trick is that I need to force every cell to re-render every time the search text changes, because a new search string means an update to the highlighted portions of the cell title. But I don't want to animate a deletion and insert, because it's still the same item. How can I tell the data source using the snapshot that it needs to reload cells?

I declare the data source like this:

@property (retain) UITableViewDiffableDataSource<NSString *, SearchHit *> *dataSource;

SearchHit represents one search result; it has properties for a display title and an array of ranges to highlight in the title. And it overrides hash and isEqual: so that every result row is uniquely identified.

My code looks something like this:

-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
  NSArray<SearchHit *> *hits = [self fetchHits:searchText];
  NSDiffableDataSourceSnapshot<NSString *, SearchHit *> *snap = [[[NSDiffableDataSourceSnapshot alloc] init] autorelease];
  [snap appendSectionsWithIdentifiers:@[@""]];
  [snap appendItemsWithIdentifiers:hits];
  [snap reloadItemsWithIdentifiers:hits];
  [self.dataSource applySnapshot:snap animatingDifferences:YES];
}

At first I didn't have the reloadItemsWithIdentifiers call there, and then no cell would change at all once it was in the result list. Adding the reload call helped, but now most of the cells are constantly one update behind. This smells like a logic error somewhere in my code, but I've verified that the hits passed to the snapshot are correct and the hits passed to the data source's cell creation callback are not.

This article by Donny Wals and this related Twitter thread involving Steve Breen suggests that the way to fix this is to make the item identifier type only represent the properties needed to display the cell. So I updated SearchHit's hash and equality comparison to include the highlighted portions of the title, which they didn't before. Then I got delete and insert animations for all the cells on every update, which I don't want.

This seems like what reloadItemsWithIdentifiers should do...right?

Sample project here on GitHub.

Mordent answered 10/3, 2020 at 15:33 Comment(6)
How about ...animatingDifferences: NO]? – Edana
@Edana interesting, that fixes the one-behind issue. But the animation is the whole point of using the diffable data source stuff. Maybe this is worth a radar. – Mordent
The animatingDifferences parameter seems to indicate that the animation is not the whole point πŸ˜‰ – Edana
@Edana OK, fair :-). But animating the differences is the primary reason I use this API over the old [self.tableView reloadData]. If I recall correctly from the WWDC talk where it was introduced, the animatingDifferences parameter exists at least partly to avoid animating the initial appearance of data when a view is loaded, and other such edge cases? – Mordent
I've filed this as FB7621476. – Mordent
@TomHamming did you find any solution to this issue? – Barite
M
4

The proper solution to this is actually in the names of the APIs - the objects you give to the data source should be identifiers, like rowid values from a database. In my case, when the item identifiers don't represent rows in a database that I can look up, I just need to keep the state of the objects in some sort of lookup structure, so that when I call reloadItemsWithIdentifiers, I get the state for each cell from that structure, not from the object that the data source hands to me.

Mordent answered 10/12, 2021 at 20:48 Comment(0)
C
6

The diffable datasource API may not be the right tool to effect animations on cells themselves. It’s geared towards the animation of the appearance, disappearance and ordering of cells. If your data source has a change that is expressed via Hashable conformance the api will see it as a change and delete/insert etc.

My advice would be to remove the search text from the item identifier and have each cell observe the search text and effect an animation or redraw independently from the datasource.

Celestaceleste answered 10/3, 2020 at 16:24 Comment(6)
I don't need an animation on the cell itself, really - I just need the display to update without a delete/insert animation. Is this not what reloadItemsWithIdentifiers is for? I added sample code on GitHub to illustrate. – Mordent
@TomHamming what did you end up doing? – Metamathematics
@SherwinZadeh I talked to an Apple engineer about this in a WWDC lab and they seemed to think it was a bug - reloadItemsWithIdentifiers should be handling this. Until it gets fixed I'm just not animating. – Mordent
@TomHamming Hello, I'm encountering the same issue at the moment, thank you for such useful information, you're real lifesaver – Retardant
Is there still no solution to this problem? It seems to me like that such a basic functionality that this somehow MUST work? – Directory
@Directory Did you remember to call dataSource.applySnapshot after calling reloadItems? Also on iOS 15 you can call reconfigureItems instead. – Counteraccusation
M
4

The proper solution to this is actually in the names of the APIs - the objects you give to the data source should be identifiers, like rowid values from a database. In my case, when the item identifiers don't represent rows in a database that I can look up, I just need to keep the state of the objects in some sort of lookup structure, so that when I call reloadItemsWithIdentifiers, I get the state for each cell from that structure, not from the object that the data source hands to me.

Mordent answered 10/12, 2021 at 20:48 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.