Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assertion failure in -[UITableView _endCellAnimationsWithContext:] with NSFetchedResultsController

I have 2 managed object contexts: (1) created as NSMainQueueConcurrencyType that is used by the UI/main thread and (2) created as NSPrivateQueueConcurrencyType that is used by the networking. Both of these contexts go to the persistent store (i.e., I'm not using parent/child contexts).

For the view controller, I'm using a UITableViewController with a NSFetchedResultsController that uses the 1st UI managed object context.

I am merging changes from the 2nd managed object context into the 1st context by observing the NSManagedObjectContextDidSaveNotification.

The app works fine until it processes a network response that causes a new object to be inserted and an existing object to be deleted in the 2nd context. When the 2nd context is saved, the NSManagedObjectContextDidSaveNotification fires and the changes are merged into the 1st context. The delegate methods of NSFetchedResultsController are invoked and a new row is added to the table, but the row representing the deleted object is *not removed.

If I attempt other actions on the table view, like reloading the table or updating other objects, I get this assert in the console log:

*** Assertion failure in -[UITableView _endCellAnimationsWithContext:],
/SourceCache/UIKit/UIKit-2380.17/UITableView.m:1070

CoreData: error: Serious application error.  An exception was caught
from the delegate of NSFetchedResultsController during a call to -
controllerDidChangeContent:.  Invalid update: invalid number of rows in
section 0.  The number of rows contained in an existing section after the
update (6) must be equal to the number of rows contained in that section
before the update (7), plus or minus the number of rows inserted or deleted
from that section (6 inserted, 6 deleted) and plus or minus the number of rows
moved into or out of that section (0 moved in, 0 moved out). with 
userInfo (null)

Typically, you get this error if you've forgotten to update your model objects when using UITableView's batch update methods, but in this case, NSFetchedResultsController is doing all work. My delegate methods are boilerplate:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
  [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
  switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
  NSLog(@"    didChangeObject type=%d indexPath=%@ newIndexPath=%@", type, indexPath, newIndexPath);

  UITableView *tableView = self.tableView;

    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
  [self.tableView endUpdates];
}

My UITableViewDataSource tableView:cellForRowAtIndexPath method is:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  static NSString *CellIdentifier = @"Cell";

  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  if (cell == nil) {
      cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
      cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
  }

  [self configureCell:cell atIndexPath:indexPath];

  return cell;
}

And configureCell: is:

- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
  Event *event = (Event *)[self.fetchedResultsController objectAtIndexPath:indexPath];
  cell.textLabel.text = [[event valueForKey:@"timeStamp"] description];

  NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Owner"];
  NSArray *objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];
  cell.detailTextLabel.text = [objects.lastObject name]; // simplified
}
like image 364
Peyman Avatar asked Dec 26 '22 02:12

Peyman


1 Answers

NSFetchedResultsController gets terribly confused if you use an NSFetchRequest when preparing your cell in response to tableView:cellForRowAtIndexPath:. If you don't execute a NSFetchRequest, all is well. However, if you do, it triggers NSFetchedResultsController to perform further change notifications, which does bad things to UITableView.

The workaround for this is to set includesPendingChanges = NO on your NSFetchRequest.

I have opened a radar issue about this -- problem id 14048101 -- with a detailed example and sample app. This bug reproduces on iOS 5.1, 6.0, and 6.1.

In my sample app, I added logging to Xcode's CoreData template to log enter/leave of NSFetchedResultsController delegate methods. When I insert + delete objects on the network context, the logging shows:

01: => (before) mergeChangesFromContextDidSaveNotification 02: => (enter) controllerWillChangeContent count=4 03: <= (leave) controllerWillChangeContent count=4 04: didChangeObject type=1 indexPath=(null) newIndexPath= 2 indexes [0, 0] 05: => (enter) controllerDidChangeContent count=5

At this point, all is good. controllerDidChangeContent: has been called to process the 1 insert, which calls [tableView endUpdates], which calls tableView:cellForRowAtIndexPath:, which calls configureCell:atIndexPath:.

06: => (enter) configure cell at row 0

At this point, configureCell:atIndexPath: creates an NSFetchRequest and calls [self.managedObjectContext executeFetchRequest:error:] -- here begins the badness. Executing this fetch request triggers the processing of the remaining changes in the context (1 delete and 3 updates) before processing of the insert has finished (we entered controllerDidChangeContent: on line #05 and don't leave until line #16).

07: => (enter) controllerWillChangeContent count=5 08: <= (leave) controllerWillChangeContent count=5 09: didChangeObject type=2 indexPath= 2 indexes [0, 4] newIndexPath=(null) 10: didChangeObject type=4 indexPath= 2 indexes [0, 2] newIndexPath=(null) 11: didChangeObject type=4 indexPath= 2 indexes [0, 1] newIndexPath=(null) 12: didChangeObject type=4 indexPath= 2 indexes [0, 3] newIndexPath=(null) 13: => (enter) controllerDidChangeContent count=4

At this point, the framework is making a re-entrant call tocontrollerDidChangeContent:.

14: <= (leave) controllerDidChangeContent count=4 15: <= (leave) configure cell at row 0 16: <= (leave) controllerDidChangeContent count=4 17: <= (after) mergeChangesFromContextDidSaveNotification

At this point, you can see in the UI that: (1) a new cell has been added, (2) 3 cells were updated, and (3) the deleted cell is still visible, which is wrong.

After further action in the UI, I typically get an Assertion failure or message sent to an invalid object exception.

My sample app is available at https://github.com/peymano/CoreDataFetchedResultsController

like image 146
Peyman Avatar answered May 17 '23 17:05

Peyman