ios – performBatchUpdates completion handler is not called when there is section operation involved

So far, here’s are the code snippets that almost work for NSFetchedResultsController + UICollectionView, based on the information provided

Please note that, there are 2 (BlockOperation), as reloadItems and moveItem doesn’t play well within single performBatchUpdates. Based on the workaround proposed in the video, we have to call reloadItems in a separate performBatchUpdates.

We also do not follow 100% methods (Perform reloadItems typed performBatchUpdates first, followed by insert/ move/ delete typed performBatchUpdates) proposed in the video as we notice it doesn’t work well even simple case. The “almost” work method we found are

  • Perform performBatchUpdates for insert, move and delete
  • At completion handler of performBatchUpdates, perform another performBatchUpdates for reloadItems

NSFetchedResultsController + UICollectionView integration

private var blockOperations: (BlockOperation) = ()

// reloadItems and moveItem do not play well together. We are using the following workaround proposed at
// https://developer.apple.com/videos/play/wwdc2018/225/
private var blockUpdateOperations: (BlockOperation) = ()

extension DashboardViewController: NSFetchedResultsControllerDelegate {
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        
        if type == NSFetchedResultsChangeType.insert {
            print(">> insert")
            blockOperations.append(
                BlockOperation(block: { (weak self) in
                    if let self = self {
                        self.collectionView!.insertItems(at: (newIndexPath!))
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.update {
            print(">> update")
            blockUpdateOperations.append(
                BlockOperation(block: { (weak self) in
                    if let self = self, let indexPath = indexPath {
                        self.collectionView.reloadItems(at: (indexPath))
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.move {
            print(">> move")
            blockOperations.append(
                BlockOperation(block: { (weak self) in
                    if let self = self, let newIndexPath = newIndexPath, let indexPath = indexPath {
                        self.collectionView.moveItem(at: indexPath, to: newIndexPath)
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.delete {
            print(">> delete")
            blockOperations.append(
                BlockOperation(block: { (weak self) in
                    if let self = self {
                        self.collectionView!.deleteItems(at: (indexPath!))
                    }
                })
            )
        }
    }
    
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
        if type == NSFetchedResultsChangeType.insert {
            print(">> section insert")
            blockOperations.append(
                BlockOperation(block: { (weak self) in
                    if let self = self {
                        self.collectionView!.insertSections(IndexSet(integer: sectionIndex))
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.update {
            print(">> section update")
            blockOperations.append(
                BlockOperation(block: { (weak self) in
                    if let self = self {
                        self.collectionView!.reloadSections(IndexSet(integer: sectionIndex))
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.delete {
            print(">> section delete")
            blockOperations.append(
                BlockOperation(block: { (weak self) in
                    if let self = self {
                        self.collectionView!.deleteSections(IndexSet(integer: sectionIndex))
                    }
                })
            )
        }
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        if blockOperations.isEmpty {
            performBatchUpdatesForUpdateOperations()
        } else {
            collectionView.performBatchUpdates({ (weak self) () -> Void  in
                guard let self = self else { return }
                
                for operation: BlockOperation in self.blockOperations {
                    operation.start()
                }
                
                self.blockOperations.removeAll(keepingCapacity: false)
            }, completion: { (weak self) (finished) -> Void in
                guard let self = self else { return }
                
                self.performBatchUpdatesForUpdateOperations()
            })
        }
    }
    
    private func performBatchUpdatesForUpdateOperations() {
        if blockUpdateOperations.isEmpty {
            return
        }
        
        collectionView.performBatchUpdates({ (weak self) () -> Void  in
            guard let self = self else { return }
            
            for operation: BlockOperation in self.blockUpdateOperations {
                operation.start()
            }
            
            self.blockUpdateOperations.removeAll(keepingCapacity: false)
        }, completion: { (weak self) (finished) -> Void in
            print("blockUpdateOperations completed")
            
            guard let self = self else { return }
        })
    }
}

The above way, works “almost” well when no “section” operations involved.

enter image description here

For the above animation, you will observe logging

blockOperations completed
>> move
blockOperations completed
>> move
blockOperations completed

However, when a section is being added/ removed, the completion handler of performBatchUpdates is not being called!

enter image description here

For the above animation, you will observe logging

>> section delete
>> move
>> section insert
>> move

Does anyone know why it is so, and how I can workaround with this issue? Thanks.