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 }