Reputation: 509
I have implemented a UITableViewController with a UITableView driven by the results of two NSFetchedResultsControllers (a table with two sections, one section for each NSFetchedResultsControllers), as previously discussed here.
My code runs correctly and without error if both NSFetchedResultsControllers do not share any results in common.
However, I'm running into this problem when I insert a Core Data object that shows up as a result in both of my NSFetchedResultsControllers:
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 (1) must be equal to the number of rows contained
in that section before the update (1), plus or minus the number of rows inserted or
deleted from that section (1 inserted, 0 deleted). with userInfo (null)
I'm outputting whenever I'm asked for the number of rows in a section:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo;
if (section < [self.fetchedResultsController1.sections count]) {
sectionInfo = [[self.fetchedResultsController1 sections] objectAtIndex:section];
}
else {
sectionInfo = [[self.fetchedResultsController2 sections] objectAtIndex:section-[self.fetchedResultsController1.sections count]];
}
NSLog(@"Section %d with %d row(s)", section, [sectionInfo numberOfObjects]);
return [sectionInfo numberOfObjects];
}
and printing out whenever an object gets inserted in didObject:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(@"Inserting %@ %@", newIndexPath, anObject);
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
// Reloading the section inserts a new row and ensures that titles are updated appropriately.
[tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
When I run with the above code, I get the following output (abbreviated for clarity), with the above error immediately follow this:
2012-03-28 00:59:55.611 MyApp[517:207] Section 1 with 0 row(s)
2012-03-28 00:59:55.612 MyApp[517:207] Section 0 with 0 row(s)
2012-03-28 00:59:55.613 MyApp[517:207] Inserting <NSIndexPath 0x5948150> 2 indexes [0, 0] <MyClass: 0xd60f8b0> (entity: MyClass; id: 0xd611d50 <x-coredata:///MyClass/tD7E0DB3C-4D85-468C-9DD0-BAD0A53B20A310> ; data: {
....
})
2012-03-28 00:59:55.614 MyApp[517:207] Section 0 with 1 row(s)
2012-03-28 00:59:55.614 MyApp[517:207] Section 1 with 0 row(s)
2012-03-28 00:59:55.618 MyApp[517:207] Inserting <NSIndexPath 0x5948150> 2 indexes [0, 0] <MyClass: 0xd60f8b0> (entity: MyClass; id: 0xd611d50 <x-coredata:///MyClass/tD7E0DB3C-4D85-468C-9DD0-BAD0A53B20A310> ; data: {
....
})
2012-03-28 00:59:55.619 MyApp[517:207] Section 0 with 1 row(s)
2012-03-28 00:59:55.620 MyApp[517:207] Section 1 with 1 row(s)
2012-03-28 00:59:55.620 MyApp[517:207] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1448.89/UITableView.m:995
2012-03-28 00:59:55.621 MyApp[517:207] 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 (1) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted). with userInfo (null)
Is having two NSFetchedResultsControllers driving a single UITableView whose results may overlap not a good idea? That's an acceptable answer, but if this should be possible, what can I do to avoid this error?
Upvotes: 1
Views: 627
Reputation: 90117
I assume that you wrap the changes which happen in controller:didChangeObject:...
in [tableView beginUpdates]
and [tableView endUpdates]
calls in the appropriate NSFetchedResultsControllerDelegate
methods.
So each NSFRC will call beginUpdates
, insertCells
, and endUpdates
. In this order.
Most likely the problem is that the tableView requests the number of sections and the number of rows for each section after the first call to endUpdates
. At this point in one section the amount of cells matches the result from tableView:numberOfRowsInSection:
. Unfortunately it doesn't match in the other section because the second fetchedResultsController didn't execute its delegate methods. So the second NSFRC couldn't insert new cells yet but it already returns its new number of objects.
So how to solve this. I can think of three different ways. First one is the best, last one should not be done at all.
Option 1: Rewrite your data model so one NSFetchedResultsController will work for you. Just put whatever attribute you use to decide in which of your 2 NSFRCs an object belongs into the data model and set an appropriate sectionKeyPath. You probably need a third section for objects that belong into both sections because one object can't be in two sections at the same time.
Option 2: Dump the NSFetchedResultsController and use NSArrays. If core-data changes could happen from somewhere else (e.g. an import that runs in the background) you will have a hard time because you have to track those changes yourself. You have to send notifications when another process changes core-data objects. You have to manage the changes that happen from your viewController too. You could make a copy of the old array, fetch a new array, compare the arrays and insert or delete cells where appropriate.
Option 3: If you like bad code and dirty tricks you could try to do something like this pseudocode.
in controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:
if (change will cause insert or delete in both sections) {
if (!delayEndUpdates) {
self.delayEndUpdates = YES
self.firstUpdatingController = controller;
[tableView beginUpdates]; // another beginUpdates should stop the tableView from updating. At least in theory
}
}
else {
self.delayEndUpdates = NO;
}
in controllerDidChangeContent:
[tableView endUpdates]; // the original
if (self.firstUpdatingController != controller) {
// after the second NSFRC did change its content
if (delayEndUpdates) {
[tableView endUpdates]; // another one to start tableView updating again
}
self.firstUpdatingController = nil;
}
the idea is to wrap all changes that cause inserts or deletions in both sections in another set of beginUpdates and endUpdates. However, I am not sure if this works. The documentation says that you can nest beginUpdates and endUpdates, but I have never tried.
Upvotes: 1