Search code examples
swiftanimationuicollectionviewnsfetchedresultscontroller

CollectionView scroll to newly inserted item


I've got a CollectionView driven by a NSFetchedResultsController.

The CollectionViewLayout is an horizontal "carousel" layout of cells ordered by ascending names.

New items are inserted with customs animations.

 func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {

if type == NSFetchedResultsChangeType.Insert {
    println("Insert Object: \(newIndexPath)")

   UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveEaseOut, animations: {
       self.collectionView?.insertItems(at: [newIndexPath])
   }, completion: { finished in
       self.collectionView?.scrollToItem(at: newIndexPath, at: .centeredHorizontally, animated: true)
})
    )
...

It works but the animation is kind of glitchy and the scroll happening at the same time.

I would like to scroll to Item then insert it with custom animation but if I scroll to Item before inserting it the app crash.

What is the right thing to do here?

Thanks


Solution

  • The first thing I would try is replace animate(withDuration:delay:options:animations:completion:) with performBatchUpdates(_:completion:)

    func controller(controller: NSFetchedResultsController,
                    didChangeObject anObject: AnyObject,
                    atIndexPath indexPath: NSIndexPath?,
                    forChangeType type: NSFetchedResultsChangeType,
                    newIndexPath: NSIndexPath?)
    {
    
        if type == NSFetchedResultsChangeType.Insert {
            println("Insert Object: \(newIndexPath)")
    
        self.collectionView?.performBatchUpdates({
            self.collectionView?.insertItems(at: [newIndexPath])
        }, completion: { finished in
            self.collectionView?.scrollToItem(at: newIndexPath, at: .centeredHorizontally, animated: true)
        })
        …
    }
    

    If that is still giving you problems, then you can call the scroll in the next run loop.

    func controller(controller: NSFetchedResultsController,
                    didChangeObject anObject: AnyObject,
                    atIndexPath indexPath: NSIndexPath?,
                    forChangeType type: NSFetchedResultsChangeType,
                    newIndexPath: NSIndexPath?)
    {
    
        if type == NSFetchedResultsChangeType.Insert {
            println("Insert Object: \(newIndexPath)")
    
        self.collectionView?.performBatchUpdates({
            self.collectionView?.insertItems(at: [newIndexPath])
        }, completion: { finished in
            DispatchQueue.main.async { // Defer to next runlop.
                self.collectionView?.scrollToItem(at: newIndexPath, at: .centeredHorizontally, animated: true)
            }
        })
        …
    }
    

    Finally, you could try only animating to scroll part.

    func controller(controller: NSFetchedResultsController,
                    didChangeObject anObject: AnyObject,
                    atIndexPath indexPath: NSIndexPath?,
                    forChangeType type: NSFetchedResultsChangeType,
                    newIndexPath: NSIndexPath?)
    {
    
        if type == NSFetchedResultsChangeType.Insert {
            println("Insert Object: \(newIndexPath)")
    
        self.collectionView?.reloadItems(at: [newIndexPath]) // reload without animating.
    
        DispatchQueue.main.async { // Defer to next runlop.
            self.collectionView?.scrollToItem(at: newIndexPath, at: .centeredHorizontally, animated: true)
        }
        …
    }