How do I implement drop operation in UICollectionView drag and drop with an async data source and cell registration?

I have a UICollectionView tied to a UICollectionViewDiffableDataSource, and I'm generating and applying snapshots to the datasource on a background serial queue, and generating cells using UICollectionViewCellRegistration. I'm working on supporting reordering of the contents of the collection view via drag and drop, and I'm having trouble with what to do in collectionView:performDropWithCoordinator: so the reorder animation looks right.

Normally, I would do something like this:

-(void)collectionView:(UICollectionView *)collectionView performDropWithCoordinator:(id<UICollectionViewDropCoordinator>)coordinator
{
  NSIndexPath *sourcePath = (NSIndexPath *)coordinator.items.firstObject.dragItem.localObject;
  NSInteger fromIndex = sourcePath.item;
  NSInteger toIndex = coordinator.destinationIndexPath.item;
  NSNumber *fromItem = [self.datasource itemIdentifierForIndexPath:sourcePath];
  NSNumber *toItem = [self.datasource itemIdentifierForIndexPath:coordinator.destinationIndexPath];

  //Do the move in the data model
  [MyModel moveItemFrom:fromIndex to:toIndex];

  //Do the move in the datasource. This is the data source equivalent of:
  //[collectionView moveItemAtIndexPath:sourcePath toIndexPath:coordinator.destinationIndexPath];

  NSDiffableDataSourceSnapshot *snap = self.datasource.snapshot;
  if (toIndex < fromIndex)
    [snap moveItemWithIdentifier:fromItem beforeItemWithIdentifier:toItem];
  else
    [snap moveItemWithIdentifier:fromItem afterItemWithIdentifier:toItem];

  [self.dataSource applySnapshot:snap animated:YES];

  //Drop the item
  [coordinator dropItem:coordinator.items.firstObject.dragItem toItemAtIndexPath:coordinator.destinationIndexPath];
}

But because my datasource updates happen on a background queue, I have to do at least the snapshot generation and application asynchronously, and I'd like to do the actual data model modification there too to avoid hangs. And I need to call dropItem on the coordinator on the main queue in this method. This results in an odd animation where the dropped item momentarily disappears (when drop is called) and then reappears (when the data source is updated on the background queue).

The best idea I have so far is to use UICollectionViewDropPlaceholder to hold the place in the collection view until the data source is updated. But to create a placeholder I need a cell reuse identifier (docs on init method), and I don't have one of those because I'm creating my cells using cell registrations.

So my question: what do I do in the performDrop method to make this work correctly? If the placeholder is the right idea, how do I use it in this situation?

Add a Comment

Replies

Even when you're using the new cell registration API, you can still manually register reuse identifiers using the older API. This will allow you to use drop placeholders. Cell registrations are not mutually exclusive with manual reuse identifiers and dequeueing, you can mix & match if needed.

  • So I would just duplicate my cell registration with a reuse identifier for this purpose?

  • Ok I set up reuse identifiers and tried a placeholder:

    id<UICollectionViewDropPlaceholderContext> context = [coordinator dropItem:coordinator.items.firstObject.dragItem toPlaceholder:placeholder];

    And I get an exception when calling dropItem:toPlaceholder::

    'NSInternalInconsistencyException', reason: 'UICollectionView must be updated via the UICollectionViewDiffableDataSource APIs when acting as the UICollectionView's dataSource: please do not call mutation APIs directly on UICollectionView. <UICollectionView: 0x108972000; frame = (0 152.5; 375 514.5); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x281c55830>; layer = <CALayer: 0x28125c760>; contentOffset: {0, 0}; contentSize: {375, 12814}; adjustedContentInset: {0, 0, 0, 0}; layout: <UICollectionViewCompositionalLayout: 0x107d727b0>; dataSource: <__UIDiffableDataSource 0x282dea850: sectionCounts=[_UIDataSourceSnapshotter - 0x2813e2fa0:(0:215)]; sections=[0x28129d7c0]; identifiers=[0x28129d3a0]>>'

    So UICollectionViewPlaceholder can't be used with diffable data sources?

Add a Comment