NSFetchedResultsContollerDelegate for CollectionView

前端 未结 6 1065
悲哀的现实
悲哀的现实 2020-11-30 21:10

I\'d like to use the NSFetchedResultsControllerRelegate in a CollectionViewController. Therefore I just changed the method for the TableViewController for the CollectionView

相关标签:
6条回答
  • 2020-11-30 21:15

    Combining a fetched results controller with a collection view is a bit tricky. The problem is explained in

    • http://ashfurrow.com/blog/how-to-use-nsfetchedresultscontroller-with-uicollectionview

    If you're looking for how to get around the NSInternalInconsistencyException runtime exception with UICollectionView, I have an example on GitHub detailing how to queue updates from the NSFetchedResultsControllerDelegate.

    The problem is that the existing UITableView class uses beginUpdates and endUpdates to submit batches to the table view. UICollectionView has a new performBatchUpdates: method, which takes a block parameter to update the collection view. That's sexy, but it doesn't work well with the existing paradigm for NSFetchedResultsController.

    Fortunately, that article also provides a sample implementation:

    • https://github.com/AshFurrow/UICollectionView-NSFetchedResultsController

    From the README:

    This is an example of how to use the new UICollectionView with NSFetchedResultsController. The trick is to queue the updates made through the NSFetchedResultsControllerDelegate until the controller finishes its updates. UICollectionView doesn't have the same beginUpdates and endUpdates that UITableView has to let it work easily with NSFetchedResultsController, so you have to queue them or you get internal consistency runtime exceptions.

    0 讨论(0)
  • 2020-11-30 21:24

    I made @Plot's solution it's own object and converted it to Swift 2

    import Foundation
    import CoreData
    
    class CollectionViewFetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate {
    
        // MARK: Properties
    
        private let collectionView: UICollectionView
        private var blockOperations: [NSBlockOperation] = []
    
        // MARK: Init
    
        init(collectionView: UICollectionView) {
            self.collectionView = collectionView
        }
    
        // MARK: Deinit
    
        deinit {
            blockOperations.forEach { $0.cancel() }
            blockOperations.removeAll(keepCapacity: false)
        }
    
        // MARK: NSFetchedResultsControllerDelegate
    
        func controllerWillChangeContent(controller: NSFetchedResultsController) {
            blockOperations.removeAll(keepCapacity: false)
        }
    
        func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    
            switch type {
    
            case .Insert:
                guard let newIndexPath = newIndexPath else { return }
                let op = NSBlockOperation { [weak self] in self?.collectionView.insertItemsAtIndexPaths([newIndexPath]) }
                blockOperations.append(op)
    
            case .Update:
                guard let newIndexPath = newIndexPath else { return }
                let op = NSBlockOperation { [weak self] in self?.collectionView.reloadItemsAtIndexPaths([newIndexPath]) }
                blockOperations.append(op)
    
            case .Move:
                guard let indexPath = indexPath else { return }
                guard let newIndexPath = newIndexPath else { return }
                let op = NSBlockOperation { [weak self] in self?.collectionView.moveItemAtIndexPath(indexPath, toIndexPath: newIndexPath) }
                blockOperations.append(op)
    
            case .Delete:
                guard let indexPath = indexPath else { return }
                let op = NSBlockOperation { [weak self] in self?.collectionView.deleteItemsAtIndexPaths([indexPath]) }
                blockOperations.append(op)
    
            }
        }
    
        func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    
            switch type {
    
            case .Insert:
                let op = NSBlockOperation { [weak self] in self?.collectionView.insertSections(NSIndexSet(index: sectionIndex)) }
                blockOperations.append(op)
    
            case .Update:
                let op = NSBlockOperation { [weak self] in self?.collectionView.reloadSections(NSIndexSet(index: sectionIndex)) }
                blockOperations.append(op)
    
            case .Delete:
                let op = NSBlockOperation { [weak self] in self?.collectionView.deleteSections(NSIndexSet(index: sectionIndex)) }
                blockOperations.append(op)
    
            default: break
    
            }
        }
    
        func controllerDidChangeContent(controller: NSFetchedResultsController) {
            collectionView.performBatchUpdates({
                self.blockOperations.forEach { $0.start() }
            }, completion: { finished in
                self.blockOperations.removeAll(keepCapacity: false)
            })
        }
    
    }
    

    Usage:

    fetchedResultsController.delegate = CollectionViewFetchedResultsControllerDelegate(collectionView)
    

    Swift 4 version

    private var blockOperations: [BlockOperation] = []
    
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        blockOperations.removeAll(keepingCapacity: false)
    }
    
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                    didChange anObject: Any,
                    at indexPath: IndexPath?,
                    for type: NSFetchedResultsChangeType,
                    newIndexPath: IndexPath?) {
    
        let op: BlockOperation
        switch type {
        case .insert:
            guard let newIndexPath = newIndexPath else { return }
            op = BlockOperation { self.collectionView.insertItems(at: [newIndexPath]) }
    
        case .delete:
            guard let indexPath = indexPath else { return }
            op = BlockOperation { self.collectionView.deleteItems(at: [indexPath]) }
        case .move:
            guard let indexPath = indexPath,  let newIndexPath = newIndexPath else { return }
            op = BlockOperation { self.collectionView.moveItem(at: indexPath, to: newIndexPath) }
        case .update:
            guard let indexPath = indexPath else { return }
            op = BlockOperation { self.collectionView.reloadItems(at: [indexPath]) }
        }
    
        blockOperations.append(op)
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        collectionView.performBatchUpdates({
            self.blockOperations.forEach { $0.start() }
        }, completion: { finished in
            self.blockOperations.removeAll(keepingCapacity: false)
        })
    }
    
    0 讨论(0)
  • 2020-11-30 21:27

    Here's a bit of Swift that works with UICollectionViewController's installsStandardGestureForInteractiveMovement and is a somewhat DRYed up and switches on the installsStandardGestureForInteractiveMovement so that all the code paths are obvious. It's the same overall pattern as Plot's code.

    var fetchedResultsProcessingOperations: [NSBlockOperation] = []
    
    private func addFetchedResultsProcessingBlock(processingBlock:(Void)->Void) {
        fetchedResultsProcessingOperations.append(NSBlockOperation(block: processingBlock))
    }
    
    func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    
        switch type {
        case .Insert:
            addFetchedResultsProcessingBlock {self.collectionView!.insertItemsAtIndexPaths([newIndexPath!])}
        case .Update:
            addFetchedResultsProcessingBlock {self.collectionView!.reloadItemsAtIndexPaths([indexPath!])}
        case .Move:
            addFetchedResultsProcessingBlock {
                // If installsStandardGestureForInteractiveMovement is on
                // the UICollectionViewController will handle this on its own.
                guard !self.installsStandardGestureForInteractiveMovement else {
                    return
                }
                self.collectionView!.moveItemAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
            }
        case .Delete:
            addFetchedResultsProcessingBlock {self.collectionView!.deleteItemsAtIndexPaths([indexPath!])}
        }
    
    }
    
    func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    
        switch type {
        case .Insert:
            addFetchedResultsProcessingBlock {self.collectionView!.insertSections(NSIndexSet(index: sectionIndex))}
        case .Update:
            addFetchedResultsProcessingBlock {self.collectionView!.reloadSections(NSIndexSet(index: sectionIndex))}
        case .Delete:
            addFetchedResultsProcessingBlock {self.collectionView!.deleteSections(NSIndexSet(index: sectionIndex))}
        case .Move:
            // Not something I'm worrying about right now.
            break
        }
    
    }
    
    func controllerDidChangeContent(controller: NSFetchedResultsController) {
        collectionView!.performBatchUpdates({ () -> Void in
            for operation in self.fetchedResultsProcessingOperations {
                operation.start()
            }
            }, completion: { (finished) -> Void in
                self.fetchedResultsProcessingOperations.removeAll(keepCapacity: false)
        })
    }
    
    deinit {
        for operation in fetchedResultsProcessingOperations {
            operation.cancel()
        }
    
        fetchedResultsProcessingOperations.removeAll()
    }
    
    0 讨论(0)
  • 2020-11-30 21:32

    Here is my implementation with Swift. First initialise an array of NSBlockOperations:

    var blockOperations: [NSBlockOperation] = []
    

    In controller will change, re-init the array:

    func controllerWillChangeContent(controller: NSFetchedResultsController) {
        blockOperations.removeAll(keepCapacity: false)
    }
    

    In the did change object method:

        func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    
        if type == NSFetchedResultsChangeType.Insert {
            println("Insert Object: \(newIndexPath)")
    
            blockOperations.append(
                NSBlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.insertItemsAtIndexPaths([newIndexPath!])
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.Update {
            println("Update Object: \(indexPath)")
            blockOperations.append(
                NSBlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.reloadItemsAtIndexPaths([indexPath!])
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.Move {
            println("Move Object: \(indexPath)")
    
            blockOperations.append(
                NSBlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.moveItemAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.Delete {
            println("Delete Object: \(indexPath)")
    
            blockOperations.append(
                NSBlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.deleteItemsAtIndexPaths([indexPath!])
                    }
                })
            )
        }
    }
    

    In the did change section method:

    func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    
        if type == NSFetchedResultsChangeType.Insert {
            println("Insert Section: \(sectionIndex)")
    
            blockOperations.append(
                NSBlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.insertSections(NSIndexSet(index: sectionIndex))
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.Update {
            println("Update Section: \(sectionIndex)")
            blockOperations.append(
                NSBlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.reloadSections(NSIndexSet(index: sectionIndex))
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.Delete {
            println("Delete Section: \(sectionIndex)")
    
            blockOperations.append(
                NSBlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.deleteSections(NSIndexSet(index: sectionIndex))
                    }
                })
            )
        }
    }
    

    And finally, in the did controller did change content method:

    func controllerDidChangeContent(controller: NSFetchedResultsController) {        
        collectionView!.performBatchUpdates({ () -> Void in
            for operation: NSBlockOperation in self.blockOperations {
                operation.start()
            }
        }, completion: { (finished) -> Void in
            self.blockOperations.removeAll(keepCapacity: false)
        })
    }
    

    I personally added some code in the deinit method as well, in order to cancel the operations when the ViewController is about to get deallocated:

    deinit {
        // Cancel all block operations when VC deallocates
        for operation: NSBlockOperation in blockOperations {
            operation.cancel()
        }
    
        blockOperations.removeAll(keepCapacity: false)
    }
    
    0 讨论(0)
  • 2020-11-30 21:37

    2019 Version of Plot's answer:

    var blockOperations: [BlockOperation] = []
    
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        blockOperations.removeAll(keepingCapacity: false)
    }
    
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        if type == NSFetchedResultsChangeType.insert {
            print("Insert Object: \(newIndexPath)")
    
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.insertItems(at: [newIndexPath!])
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.update {
            print("Update Object: \(indexPath)")
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.reloadItems(at: [indexPath!])
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.move {
            print("Move Object: \(indexPath)")
    
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.moveItem(at: indexPath!, to: newIndexPath!)
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.delete {
            print("Delete Object: \(indexPath)")
    
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.deleteItems(at: [indexPath!])
                    }
                })
            )
        }
    }
    
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
        if type == NSFetchedResultsChangeType.insert {
            print("Insert Section: \(sectionIndex)")
    
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.insertSections(IndexSet(integer: sectionIndex))
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.update {
            print("Update Section: \(sectionIndex)")
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.reloadSections(IndexSet(integer: sectionIndex))
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.delete {
            print("Delete Section: \(sectionIndex)")
    
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.deleteSections(IndexSet(integer: sectionIndex))
                    }
                })
            )
        }
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        collectionView!.performBatchUpdates({ () -> Void in
            for operation: BlockOperation in self.blockOperations {
                operation.start()
            }
        }, completion: { (finished) -> Void in
            self.blockOperations.removeAll(keepingCapacity: false)
        })
    }
    
    deinit {
        // Cancel all block operations when VC deallocates
        for operation: BlockOperation in blockOperations {
            operation.cancel()
        }
    
        blockOperations.removeAll(keepingCapacity: false)
    }
    
    0 讨论(0)
  • 2020-11-30 21:41

    A version for 2020:

    Based on the incredible answers above,

    Which matches the familiar Apple example for tables:

    Consider the familiar Apple example for table views:

    https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/nsfetchedresultscontroller.html#//apple_ref/doc/uid/TP40001075-CH8-SW1

    at the heading

    "Communicating Data Changes to the Table View" ...

    So,

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
        case .insert:
            insertRows(at: [newIndexPath!], with: .fade)
        case .delete:
            deleteRows(at: [indexPath!], with: .fade)
        case .update:
            reloadRows(at: [indexPath!], with: .fade)
        case .move:
            moveRow(at: indexPath!, to: newIndexPath!)
        }
    }
    

    .

    Here's the "similar pattern" to copy and paste for collection views, with current syntax etc.

    var ops: [BlockOperation] = []
    
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
            case .insert:
                ops.append(BlockOperation(block: { [weak self] in
                    self?.insertItems(at: [newIndexPath!])
                }))
            case .delete:
                ops.append(BlockOperation(block: { [weak self] in
                    self?.deleteItems(at: [indexPath!])
                }))
            case .update:
                ops.append(BlockOperation(block: { [weak self] in
                    self?.reloadItems(at: [indexPath!])
                }))
            case .move:
                ops.append(BlockOperation(block: { [weak self] in
                    self?.moveItem(at: indexPath!, to: newIndexPath!)
                }))
            @unknown default:
                break
        }
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        performBatchUpdates({ () -> Void in
            for op: BlockOperation in self.ops { op.start() }
        }, completion: { (finished) -> Void in self.ops.removeAll() })
    }
    
    deinit {
        for o in ops { o.cancel() }
        ops.removeAll()
    }
    

    .

    (I have just left out the "sections" material, which is the same.)

    Do nothing in controllerWillChangeContent?

    In the magnificent answer by @PhuahYeeKeat , in controllerWillChangeContent the ops array is cleaned out. I may be wrong but there's no reason to do that; it is reliably emptied by the batch updates cycle. Simply do nothing in controllerWillChangeContent.

    Is there a race?

    I have a concern about what happens if a new didChange arrives while the performBatchUpdates is processing the previous batch.

    I really don't know if performBatchUpdates makes a local copy or what - in which case, the global one should be deleted before doing performBatchUpdates ?

    IDK.

    0 讨论(0)
提交回复
热议问题