Tuesday, February 16, 2010

Hideous bug in NSFetchedResultsController (iPhone OS 3.1 and later)

The NSFetchedResultsController class is magical. If you're using core data, you can create one with only a few lines of code. Then setup the proper delegate methods, and you get table updates with animation! How cool is that?!

Until it starts crashing randomly, with cryptic error messages that don't make a lot of sense...

Wouldn't it be cool if somebody subclassed NSFetchedResultsController, and altered it so you can fix the problem with minimal changes while allowing you to keep your animations?

That's exactly what we did. Read on for more details.

The following applies to version 3.1 through 3.1.3 (the latest version at the time of this writing).

So what is going wrong and how can we fix it?

You have a table with sections, that starts out looking something like this:

(I overlaid the indexPath of each cell using photoshop. It is expressed as "[<section>, <row>]")

Then the user at [0, 0] becomes available, and the transition is *supposed* to end up like this:

But instead, your application crashes!

The error message is something confusing like
Serious application error. Exception was caught during Core Data change processing: *** -[NSCFArray objectAtIndex:]: index (1) beyond bounds (1) with userInfo (null)

What happened?

The NSFetchedResultsController sent the following messages:

- controllerWillChangeContent:
- controller:didChangeSection:atIndex:0 forChangeType:Insert
- controller:didChangeObject:atIndexPath:[0, 0] forChangeType:Update newIndexPath:nil
- controllerDidChangeContent:

In other words, NSFRC processed the change as an Update instead of a Move. It got confused because the previousIndexPath and newIndexPath were the same.

If you catch the mistake, and process the change as a move then everything works as expected.

The same problem occurs if we reverse the change above. This time the crash message is different:
Serious application error. Exception was caught during Core Data change processing: Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (4) 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 (0 inserted, 0 deleted). with userInfo (null)

Again, the NSFRC sent the change as an update rather than a move. Processing the change as a move will fix the problem.

Now the above solution should fix the problem IF you are dealing with only a single change. The situation gets out of hand really fast if there are multiple inserted or deleted sections. I created a test app with several unit tests, but was unable to support animation in every single test case. There were some situations that required specific knowledge of the application architecture or of the underlying data. For these cases, I created a new delegate method.

How to use the code:

First download and add the SafeFetchedResultsController class to your project.
Then change your code from this:

fetchedResultsController = [[NSFetchedResultsController alloc] ...
fetchedResultsController.delegate = self;
to this:

fetchedResultsController = [[SafeFetchedResultsController alloc] ...
fetchedResultsController.safeDelegate = self;
And then simply add a new delegate method:

- (void)controllerDidMakeUnsafeChanges:(NSFetchedResultsController *)controller
[self.tableView reloadData];

And that's it! You're done!

Download the SafeFetchedResultsController class and test project.



Nigel said...

Will using this subclass of NSFetchedResultsController be accepted by Apple reviewers?

Ariel Camus said...

I love you!

Dylan said...

Thank you! I'm going to fight Ariel to have your children.

Seriously, I've been fighting this for a month. I kept coming back to it and then saying, "screw it, I'll just reloadData." Your fix is working great so far.

Do you know if Apple fixed this in 4.0? I'm developing for the iPad so I haven't done anything serious with 4.

Matthias said...

I love you, too. Thank you very much.

vivek bolla said...

Thanks a million, your fix saved me a ton of time..