Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reordering Cells with UICollectionViewDiffableDataSource and NSFetchedResultsController

I'm using a UICollectionViewDiffableDataSource and a NSFetchedResultsController to populate my UICollectionView inside my UIViewController.

To add the ability of reordering cells I added a UILongPressGestureRecognizer and subclassed UICollectionViewDiffableDataSource in order to use it's canMoveItemAt: and moveItemAt: methods.

When reordering a cell the following things happen:

  1. moveItemAt: is called and I update the objects position property and save the MOC
  2. controllerDidChangeContent: of the NSFetchedResultsControllerDelegate is called and I create a new snapshot from the current fetchedObjects and apply it.

When I apply dataSource?.apply(snapshot, animatingDifferences: true) the cells switch positions back immediately. If I set animatingDifferences: false it works, but all cells are reloaded visibly.

Is there any best practice here, how to implement cell reordering on a UICollectionViewDiffableDataSource and a NSFetchedResultsController?

Here are my mentioned methods:

// ViewController 
func createSnapshot(animated: Bool = true) {
    var snapshot = NSDiffableDataSourceSnapshot<Int, Favorite>()
    snapshot.appendSections([0])
    snapshot.appendItems(provider.fetchedResultsController.fetchedObjects ?? [])
    dataSource?.apply(snapshot, animatingDifferences: animated)
}

// NSFetchedResultsControllerDelegate
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    createSnapshot(animated: false)
}

// Subclassed UICollectionViewDiffableDataSource
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
    provider.moveFavorite(from: sourceIndexPath.row, to: destinationIndexPath.row)
}

// Actual cell moving in a provider class
public func moveFavorite(from source: Int, to destination: Int) {
    guard let favorites = fetchedResultsController.fetchedObjects else { return }
    if source < destination {
        let partialObjects = favorites.filter({ $0.position <= destination && $0.position >= source })

        for object in partialObjects {
            object.position -= 1
        }

        let movedFavorite = partialObjects.first
        movedFavorite?.position = Int64(destination)
    }
    else {
        let partialObjects = favorites.filter({ $0.position >= destination && $0.position <= source })

        for object in partialObjects {
            object.position += 1
        }

        let movedFavorite = partialObjects.last
        movedFavorite?.position = Int64(destination)
    }
    do {
        try coreDataHandler.mainContext.save()
    } catch let error as NSError {
        print(error.localizedDescription)
    }
}
like image 272
mrtnlst Avatar asked Sep 20 '19 13:09

mrtnlst


2 Answers

My solution to the same issue is to subclass the UICollectionViewDiffableDataSource and implement the canMoveItemAt method in the subclass to answer true. The animation seems to work fine for me if the longPressAction case of .ended does three things:

  1. update the model

  2. call dateSource.collectionView(..moveItemAt:..)

  3. run your dataSource.apply

The other usual methods for drag behavior have to be also implemented which it looks like you have done. FYI for others- These methods are well documented in the section for 'Reordering Items Interactively' of UICollectionView. https://developer.apple.com/documentation/uikit/uicollectionview

class PGLDiffableDataSource: UICollectionViewDiffableDataSource<Int, Int> {
    override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
        return true
    }

}
like image 135
Will Loew-Blosser Avatar answered Nov 18 '22 09:11

Will Loew-Blosser


No need to subclass. Starting in iOS 14.0, UICollectionViewDiffableDataSource supports reordering handlers you can implement.

let data_source = UICollectionViewDiffableDataSource<MySection, MyModelObject>( collectionView: collection_view, cellProvider:
{
    [weak self] (collection_view, index_path, video) -> UICollectionViewCell? in
        
    let cell = collection_view.dequeueReusableCell( withReuseIdentifier: "cell", for: index_path ) as! MyCollectionViewCell

    if let self = self
    {
        //setModel() is my own method to update the view in MyCollectionViewCell
        cell.setModel( self.my_model_objects[index_path.item] )
    }

    return cell
})

// Allow every item to be reordered as long as there's 2 or more
diffable_data_source.reorderingHandlers.canReorderItem = 
{ 
    item in 
    my_model_objects.count >= 2 return true 
}

//Update your model objects before the reorder occurs. 
//You can also use didReorder, but it might be useful to have your
//model objects in the correct order before dequeueReusableCell() is 
//called so you can update the cell's view with the correct model object.
diffable_data_source.reorderingHandlers.willReorder = 
{ 
    [weak self] transaction in
    
    guard let self = self else { return }
    
    self.my_model_objects = transaction.finalSnapshot.itemIdentifiers
}
like image 24
Kacy Avatar answered Nov 18 '22 08:11

Kacy