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?
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)
}
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)
})
}
Combining a fetched results controller with a collection view is a bit tricky. The problem is explained in
If you're looking for how to get around the
NSInternalInconsistencyExceptionruntime exception withUICollectionView, I have an example on GitHub detailing how to queue updates from the NSFetchedResultsControllerDelegate.The problem is that the existing
UITableViewclass usesbeginUpdatesandendUpdatesto submit batches to the table view.UICollectionViewhas a newperformBatchUpdates: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:
From the README:
This is an example of how to use the new
UICollectionViewwithNSFetchedResultsController. The trick is to queue the updates made through theNSFetchedResultsControllerDelegateuntil the controller finishes its updates.UICollectionViewdoesn't have the samebeginUpdatesandendUpdatesthatUITableViewhas to let it work easily withNSFetchedResultsController, so you have to queue them or you get internal consistency runtime exceptions.
A version for 2020:
Based on the incredible answers above and 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.)
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With