NSFetchedResultsContollerDelegate for CollectionView

心已入冬 提交于 2019-12-17 15:27:30

问题


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

(void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
       atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {

    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.collectionView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
            break;

        case NSFetchedResultsChangeDelete:
            [self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] ];

       break;
    }
}


(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
   atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath {

  UICollectionView *collectionView = self.collectionView;

  switch(type) {

    case NSFetchedResultsChangeInsert:
        [collectionView insertItemsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]];
        break;

    case NSFetchedResultsChangeDelete:
        [collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
        break;

    case NSFetchedResultsChangeUpdate:
        [collectionView reloadItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
        break;

    case NSFetchedResultsChangeMove:
        [collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
        [collectionView insertItemsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]];
        break;
  }
}

(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
   [self.collectionView reloadData];
}

But I do not know how to handle the WillChangeContent (beginUpdates for TableView) and DidChangeContent (endUpdates for TableView) for a CollectionView.

Everything works fine except when I move one item from one section to another section. Then I get the following error.

This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. Invalid update: invalid number of items in section 0....

Any idea how can I solve this issue?


回答1:


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.




回答2:


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)
}



回答3:


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)
    })
}



回答4:


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()
}



回答5:


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)
}


来源:https://stackoverflow.com/questions/20554137/nsfetchedresultscontollerdelegate-for-collectionview

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!