Monday, July 6, 2009

Core Data Navigation-Based Application

There's a problem with the Core Data Navigation-Based Application Template in 3.0. In the current 3.1 beta, the problem has been partially (but not fully) resolved (can't give details because of the NDA, sorry), but hopefully it will be fixed in the final version of 3.1.

If you use the template in 3.0 and run the sample application, the application will crash if you try to delete the last or the only row in the application.

The problem is here:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {

if (editingStyle == UITableViewCellEditingStyleDelete) {
//[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];

// Delete the managed object for the given index path
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
[context deleteObject:[fetchedResultsController objectAtIndexPath:indexPath]];


// Save the context.
NSError *error;
if (![context save:&error]) {
// Update to handle the error appropriately.
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
exit(-1); // Fail
}

[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade
]
;
}

}

What happens is that the object is deleted from the context here:

    [context deleteObject:[fetchedResultsController objectAtIndexPath:indexPath]];

it causes the Fetched Results Controller to removes the object from its resultset and the corresponding table. So, when, a few lines later, when it attempts to delete the row from the table:

        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade
]
;

The row it's trying to delete is no longer there.

The solution to this, however, is non-obvious. In fact, I didn't figure this one out myself. In the 3.0 template, the following method is commented out:

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// In the simplest, most efficient, case, reload the table view.
[self.tableView reloadData];
}

The first thing you should do is uncomment this method. You want to be getting change notifications from your fetched results controller. However, if we leave it as-is, we'll start getting that same problem when we delete every row, instead of just when we delete the last or only row. To resolve this, you need to add one line of code to the uncommented method (it's in bold):

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// In the simplest, most efficient, case, reload the table view.
if (!self.tableView.editing)
[self.tableView reloadData];
}


Once you've made that change, you should be good to go. In fact, you might want to go and change the code in the project template by making the change to the file called RootViewController.m at the following location (assuming you've installed the dev tools in the default location:

/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Project Templates/Application/Navigation-based Application/Navigation-based Core Data Application/Classes


Thanks to iPhone Developer Rod Brown of TheBarcodeProject for pointing me in the right direction on this issue!

No comments:

Post a Comment