I have an UITableViewController which is backed by an NSFetchedResultsController.
My NSFetchedResultsController put results into two sections based on a boolean.
In a background thread, the datasource is altered such that rows are added or removed. I have my background thread's NSManagedObjectContext's merging correctly and this situation work fine for most cases. Rows alter when the data is changed and move between sections with animations.
There is one situation however where my application crashes with an EXC_BAD_ACCESS.
The case is when the last row in a section is moved to the other section. (Stack trace below). The crash occurs in objc_msgSend but the normal debugging tips I use aren't returning anything helpful, I simply receive "Value can't be converted to integer" from gdb
.
The NSFetchedResultsControllerDelegate methods get called in the following order:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type;
// ^ The parameters specify a deletion of the 1st section
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath;
// ^ The parameters specify a moved row from the 1st section to the 1st section
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;
Looking through StackOverflow, many of the people who encounter similar issues are backing their UITableViewController manually using an NSArray or NSMutableArray and they're simply not updating the datasource at the correct time. In this case the NSFetchedResultsController is handling returning the number of rows and sections and is definitely updated by the time the [tableView endUpdates]
is called.
Has anyone got any debugging tips, pointers or solutions? This blog post hints at working around a similar problem where a new section is created.
I have two options if I cannot solve this issue as is:
[tableView reloadData]
I have modified the Apple sample Core Data application "Locations" to use an NSFetchedResultsController directly and reproduced the issue. I have uploaded the source code for this project. Use that project to reproduce the issue.
The crash is sometimes about creating two animations for the same cell as per other StackOverflow questions on the topic. More commonly the crash is as discussed above and below.
The stack trace:
#0 0x31e0afbc in objc_msgSend ()
#1 0x32c11522 in -[_UITableViewUpdateSupport(Private) _computeRowUpdates] ()
#2 0x32c10510 in -[_UITableViewUpdateSupport initWithTableView:updateItems:oldRowData:newRowData:oldRowRange:newRowRange:context:] ()
#3 0x32c0f99e in -[UITableView(_UITableViewPrivate) _endCellAnimationsWithContext:] ()
#4 0x32c0e66c in -[UITableView endUpdatesWithContext:] ()
#5 0x000088d6 in -[DraftHistoryController controllerDidChangeContent:] (self=0x3f6f1e0, _cmd=0x377ca29c, controller=0x3f716e0) at /Users/ataylor/Documents/Documents/Programming/iPhone/Drafter/Drafter/Drafter/DraftHistoryController.m:323
#6 0x3775a892 in -[NSFetchedResultsController(PrivateMethods) _managedObjectContextDidChange:] ()
My NSFetchedResultsControllerDelegate methods are the same ones from the Apple sample code:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[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;
}
}
- (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)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates]; // <---- Crash occurs here
}
The relevant UITableViewControllerDelegate methods are as follows:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
I took a quick look. Your modified Locations project didn't crash on me but it did generate core data exceptions. The problem lies in reloading the sections in controller:didChangeObject:. I changed the code as follows and everything looks good to me (including the section titles) on both iOS 4.3 and iOS 5.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
// other cases here
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
I was super curious about the code that fixed an issue with adding a new section so I decided to pop it in place of my current NSFetchedResultsControllerDelegate methods and it indeed solved my problem. Tracing through, I have discovered that this code from the Apple "Locations" sample crashes when using the NSFetchedResultsController:
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;
But the following code works perfectly:
case NSFetchedResultsChangeMove:
if (newIndexPath != nil) {
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath] withRowAnimation: UITableViewRowAnimationTop];
}
else {
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:[indexPath section]] withRowAnimation:UITableViewRowAnimationFade];
}
break;
I don't have a case where section headings will change, I won't continue debugging to solve the issue Apple's code purports to fix around section titles.
Because every question needs a swift answer, here's one (based on XJones's accepted answer). As is the case with the other answers, the magic is in the .Move: delete and insert instead of moving the row.
This is my NSFRC delegate didChangeObject:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Delete:
if let deletePath = indexPath {
self.tableView.deleteRowsAtIndexPaths([deletePath], withRowAnimation: .None)
}
break
case .Insert:
if let insertPath = newIndexPath {
self.tableView.insertRowsAtIndexPaths([insertPath], withRowAnimation: .Fade)
}
break
case .Update:
if let updatePath = indexPath {
self.tableView.reloadRowsAtIndexPaths([updatePath], withRowAnimation: .None)
}
break
case .Move:
if let indexPath = indexPath, newIndexPath = newIndexPath {
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
self.tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
break
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With