5

I have found a fix for this, but I'm not really liking the fix. My issue goes like this. I am using a NSFetchedResultsController to populate a UICollectionView-- which displays a collection of images. Each image is described by a Core Data object (e.g., its file name is in the Core Data object).

I have UI controls that allow a user to delete multiple images at the same time, and was having a problem when the user would delete more than one object. The code to do the deletion was:

    for image in images {
       CoreData.sessionNamed(CoreDataExtras.sessionName).remove(image)
    }

    CoreData.sessionNamed(CoreDataExtras.sessionName).saveContext()

(Some of this is my library code). With the deletion of two objects, I get a crash and the following log message:

CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (99) must be equal to the number of items contained in that section before the update (101), plus or minus the number of items inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)

What fixes the problem is if I change the deletion code to:

    for image in images {
        CoreData.sessionNamed(CoreDataExtras.sessionName).remove(image)
        CoreData.sessionNamed(CoreDataExtras.sessionName).saveContext()
    }

I guess the problem is that in the delegate callback method:

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

I do:

collectionView.deleteItems(at: [indexPath]) 

Apparently, you can either do a reloadItems in the didChangeObject method, or you can do a saveContext after each object deletion.

Chris Prince
  • 7,288
  • 2
  • 48
  • 66

1 Answers1

3

If you delete several images, and then save the context, the FRC processes all the deletions - so its sections, fetchedObjects, etc, reflect all those changes. But it then calls the didChangeObject: delegate method separately for each change. In that method, you call the collectionView update methods (eg. deleteItems); the collectionView then calls its dataSource methods and does a quick tally up: there were X items, Y items were deleted, there are now Z items and throws an error because Z != X-Y.

When a FRC is used with a tableView, this problem is overcome by using the tableView beginUpdates and endUpdates calls in the FRC controllerWillChangeContent: and controllerDidChangeContent: delegate methods. This causes the tableView to defer doing the tally up until ALL the individual changes have been processed - at which point the numbers do add up.

Your solution - to call saveContext after each deletion - causes the FRC to process each deletion in turn: updating its sections, fetchedObjects, etc, to reflect only one deletion at a time. This keeps the FRC's data in sync with the collectionView. One possible refinement would be to call processPendingChanges on the context after each deletion, instead of saving the context. This avoids saving data when you might not want to, but nonetheless causes each deletion to be processed separately.

The alternative is to mimic the tableView's beginUpdates/endUpdates mechanism for holding all the collectionView updates until all the FRC updates have been processed. This works broadly as follows:

  1. Create arrays to keep track of the changes (inserts, deletes).
  2. Each time didChangeObject: is called, add the corresponding indexPath to the relevant array.
  3. When controllerDidChangeContent: is called, iterate through the arrays (deletions first, when inserts) calling the corresponding collectionView update methods. (Then empty the arrays ready for the next batch of updates).

Some good explanations and potential implementations are included in this question and its answers.

pbasdf
  • 21,386
  • 4
  • 43
  • 75
  • Thank you! -- I'll likely make use of your `processPendingChanges` idea. – Chris Prince Dec 06 '17 at 23:43
  • It turns out that I had to move to the kind of solution you talked about with arrays-- to accumulate the pending deletions and do them all at once-- in terms of the collection view. Neither my original "fix" to the problem nor the `processPendingChanges` change completely solved my problem. – Chris Prince Dec 26 '17 at 05:58
  • Well, life went on and my crashes didn't go away. Even with these changes. It turned there was something else, the same but different, going on-- I had a second view controller also using a `NSFetchedResultsController` with the same Core Data objects. However, there was a memory leak and this view controller was being retained. When I deleted images in my main view controller, this other, retained view controller was causing the crash-- for basically the same reasons as discussed above by @pbasdf. – Chris Prince Dec 28 '17 at 05:41