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.

This is because we notice that it doesn't work well even for simple case. Some strange behaviour including reloadItems will cause duplicated cell UI to be shown on screen. The "almost" work method we found are

  1. Perform performBatchUpdates for insert, move and delete
  2. 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
                print("blockOperations completed")

                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.

If there is item moved operation between different section, the following logging will be printed

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

However, if there are item being moved and section being added/ removed, the following logging will be printed

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

This means the completion handler block is not executed! Does anyone know why it is so, and how I can workaround with this issue? Thanks.

performBatchUpdates completion handler is not called when there is section operation involved
 
 
Q