objective-cuitableviewdiffabledatasourcensdiffabledatasourcesnapshot

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


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.


Solution

  • 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.