I am trying to understand why my app crashes when implementing the NSFetchedResultsController delegate like so:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
contentCollectionView.performBatchUpdates({
switch type {
case .insert:
guard let insertIndexPath = newIndexPath else { return }
self.contentCollectionView.insertItems(at: [insertIndexPath])
case .delete:
guard let deleteIndexPath = indexPath else { return }
self.contentCollectionView.deleteItems(at: [deleteIndexPath])
case .update:
guard let updateIndexPath = indexPath else { return }
self.contentCollectionView.reloadItems(at: [updateIndexPath])
case .move:
guard let indexPath = indexPath, let newIndexPath = newIndexPath else { return }
self.contentCollectionView.moveItem(at: indexPath, to: newIndexPath)
}
}) { (completed) in
}
}
The console shows this crash error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (52) must be equal to the number of items contained in that section before the update (50), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).
From a data point of view I am not able to see what I am doing wrong on my end, core data is updated fine but the CollectionView crashes like explained above.
When I restart the app it works fine.
Anybody can tell me if the code I posted is not the right way of handling the delegate? What could I be doing wrong?
Another thing I am wondering about is if I am getting this crash because I am used to work with UITableViews and not collection views. That to say that I am not handling the "beginUpdates" or "endUpdates" like I do in the TableView. UICollectionView does not have those calls for what I just found out, so, Am I getting this crash because I am not properly handling the begin and end updated in the CollectionView? If so, what is the best solution to do that?
Addition: So, I have tried the solution suggested: https://gist.github.com/nor0x/c48463e429ba7b053fff6e277c72f8ec
It still crashes.
If I do not use the FRC delegate (so, if I don't set the FRC delegate at all) the app does not crash simply because I reload the whole collection view.
Any idea what I can do to properly use the delegate of the FRC and not crash?
Addition:
Here's another bit of information: The BlockOperation that inserts an object is actually run. I have put a log into the block:
blockOperations.append(
BlockOperation(block: { [weak self] in
dPrint("BlockOperation Insert Object: \(newIndexPath)")
if let this = self {
DispatchQueue.main.async {
this.collectionView!.insertItems(at: [newIndexPath!])
}
}
})
)
That is run in the console, so I know "insert" is called on the collection view (not empty, the collection view is not an empty section at this time), but then I get this:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (64) must be equal to the number of items contained in that section before the update (63), plus or minus the number of items inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).
So then, why does it say "... (0 inserted, 0 deleted) ... ", if the insertion did run?
Addition:
So, as you can see from my last addition above I made a mistake in claiming that the insertion happened. The log dPrint("BlockOperation Insert Object: (newIndexPath)") is before the DispatchQueue.main.async { so I couldn't actually claim the insertion actually run. I then put a breakpoint and the crash happened before the DispatchQueue.main.async { could execute, so no insertion actually! So, I removed the DispatchQueue.main.async { and left the this.collectionView!.insertItems(at: [newIndexPath!]) in. NO MORE CRASH!!!
Question is, why? Any idea?
I went ahead and implemented my own solution. The asynchronous solution proposed did not work and would still crash for me.
This is most of the code that is now able to handle the FRC delegate changes for a to drive a collection view:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
changes = []
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
guard let dataSource = dataSource else { return }
guard let collectionView = dataSource.collectionView else { return }
guard collectionView.window != nil else { return }
if type == NSFetchedResultsChangeType.insert {
if collectionView.numberOfSections > 0 {
if collectionView.numberOfItems( inSection: newIndexPath!.section ) == 0 {
self.shouldReloadCollectionView = true
} else {
changes?.append(CollectionViewChange(isSection:false, type: type, indexPath: nil, newIndexPath: newIndexPath, sectionIndex:nil))
}
} else {
self.shouldReloadCollectionView = true
}
}
else if type == NSFetchedResultsChangeType.update {
changes?.append(CollectionViewChange(isSection:false, type: type, indexPath: indexPath, newIndexPath:nil, sectionIndex:nil))
}
else if type == NSFetchedResultsChangeType.move {
changes?.append(CollectionViewChange(isSection:false, type: type, indexPath: indexPath, newIndexPath: newIndexPath, sectionIndex:nil))
}
else if type == NSFetchedResultsChangeType.delete {
if collectionView.numberOfItems( inSection: indexPath!.section ) == 1 {
self.shouldReloadCollectionView = true
} else {
changes?.append(CollectionViewChange(isSection:false, type: type, indexPath: indexPath, newIndexPath: nil, sectionIndex:nil))
}
}
}
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
guard let dataSource = dataSource else { return }
guard let collectionView = dataSource.collectionView else { return }
guard collectionView.window != nil else { return }
changes?.append(CollectionViewChange(isSection:true, type: type, indexPath: nil, newIndexPath: nil, sectionIndex:sectionIndex))
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let dataSource = dataSource else {
self.changes = nil
return
}
guard let collectionView = dataSource.collectionView else {
self.changes = nil
return
}
guard collectionView.window != nil else { self.changes = nil; return }
// Checks if we should reload the collection view to fix a bug @ http://openradar.appspot.com/12954582
if (self.shouldReloadCollectionView) {
collectionView.reloadData()
} else {
collectionView.performBatchUpdates({ () -> Void in
if let changes = self.changes {
for change in changes {
switch change.type {
case .insert:
if change.isSection {
collectionView.insertSections(NSIndexSet(index: change.sectionIndex!) as IndexSet)
} else {
collectionView.insertItems(at: [change.newIndexPath!])
}
case .update:
if change.isSection {
collectionView.reloadSections(NSIndexSet(index: change.sectionIndex!) as IndexSet)
} else {
collectionView.reloadItems(at: [change.indexPath!])
}
case .move:
if !change.isSection {
collectionView.moveItem(at: change.indexPath!, to: change.newIndexPath!)
}
case .delete:
if change.isSection {
collectionView.deleteSections(NSIndexSet(index: change.sectionIndex!) as IndexSet)
} else {
collectionView.deleteItems(at: [change.indexPath!])
}
break
}
}
}
}, completion: { (finished) -> Void in
self.changes = nil
})
}
}
deinit { changes = nil }