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!



11 comments:

Patrick said...

I reported this back in the 3.0 beta (around beta 4) and it was marked duplicate. So I wasn't the only one who found it.

I fixed it another way: if I'm deleting the last row, tell the tableview that the section has been deleted as well. The error I received was because the tableView didn't expect zero sections (which is what is returned from FetchedResultsController after the delete) without being told about a section going away.

Seems they never tested deleting the last row of a section.

Jeff LaMarche said...

Yeah, and the "fix" that they put into the 3.1 template was obviously rushed, as it was a hack that just hid the problem. *sigh* But, it's too be expected with a new class, I guess.

I'd be curious to know if this is the same problem. I've seen other people work around the delete problem by checking for 0 rows in the section and row table view delegate methods. This is certainly cleaner and simpler if it fixes the problem, since it's only one line of code.

The Fetched Results Controller is nice, but it feels a little rushed to me, and like perhaps somebody somewhere in the process didn't fully understand the design.

Daniel said...

Thanks so much for your post. Unfortunately after following your directions, my app still crashes when deleting the last row in a tableView.

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
if (!self.tableView.editing)
[self.tableView reloadData];
}

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the table view after the update (0) must be equal to the number of sections contained in the table view before the update (1), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).'

I'm currently using a 3.0 template. Are there any other additional changes that need to be done? I would really appreciate some help. This is driving me nuts!

hollowout said...

Apple had mentioned and provided an workaround for this in 3.0 SDK document: NSFetchedResultsController Class Reference

Important: On iPhone OS 3.0, if you have a single section table view, there is an incompatibility between the values returned by NSFetchedResultsController and the values expected by UITableView. You can work around this incompatibility as follows...

Daniel said...

@hollowout:

Thanks very much! That fixed my problem.

Malcolm Hall said...

Here is my bug post and my fix:

-------------------------------------------------------
Bug ID #: 6992448
Bug Title: Core Data Navigation Template Bug
-------------------------------------------------------
GMT21-Jun-2009 21:07:42GMT
Malcolm Hall:
There's a bug in the default template when you create a core data navigation app. When you run the app, add a row and then delete the row so there are none left it crashes. This is because in the fetch controller sections count becomes zero, but you can't return zero for number of sections in the table delegate method unless you called deleteSections because it exceptions. To fix make this change round about line #165 in RootViewController.m :

if([[fetchedResultsController sections] count] == 0){
[tableView deleteSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}else{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}

Reinaldo de O. Castro said...

Many thanks for your post. I had use 3.1.3 sdk and the problem still exists. I just had to do a modification, because when I was editing (and not deleting) a row, the modifications weren't show in the table view. So I had to declare a property in my controller class that was setted to YES/NO accordingly with the operation.

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
if (!isDeletingRowFromTableView) {
[self.tableView reloadData];
}
}

Jazmin said...

Hi
Thanks for the comments. I am using the SDK 3.1.2 and got the error
"*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted).'"
Which got fixed by implementing the NSFetchedResultsControllerDelegate. Just added the declaration on the header and assign self as delegate. So maybe it got resolved on this version. Thank you!

Edwin said...

scrub m65 kamagra attorney lawyer body scrub field jacket lovegra marijuana attorney injury lawyer

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels

Hire iphone developer said...

Hello,
The first thing you should do when looking to hire a freelance iPhone developer is to outline what type of development you need done for the iPhone.

Hire iphone Developer