Monday, February 22, 2010

More bugs in NSFetchedResultsController (iPhone OS 3.1 and later)

In my last post I talked about a hideous bug in NSFetchedResultsController that can cause your application to crash. After tracking down the cause of the problem, I presented a subclass named SafeFetchedResultsController that fixes the problem for you.

I've discovered another related bug in NSFetchedResultsController. While this bug doesn't cause your app to crash, it can cause your table cells to display stale, invalid or duplicate data.

Imagine you have a simple table with some data like this:



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

Now imagine you insert a new row at [0, 0] named "AAA", and at the same time you change "Zach Braff" to "Quack Braff". It should end up like this:



But if you use NSFetchedResultsController with the normal UITableView beginUpdates/endUpdates technique, then you end up with something completely different:



This is because the "Zach Braff" row was actually moved. It was originally after "Robbie Hanson", and then moved in front. ("Quack" < "Robbie"). So it should be processed as a move. However, due to the insert, the row's indexPath actually ends up in the same place: [0, 3]. The NSFetchedResultsController saw this, and mistook it for an update.

And so the NSFRC fired notifications of an insert at [0, 0] and an update at [0, 3]. The UITableView then did it's proper duty, and inserted a table row at [0, 0], and updated the table cell that was then at [0, 4]. Oops! If the NSFRC had properly reported the change as a move, then everything would have worked perfectly.

The SafeFetchedResultsController will inspect all updates, and will alter any changes that it thinks might have actually been moves. In other words, if an "update" could actually be a "move", then the SafeFetchedResultsController will set both the indexPath and newIndexPath for the change.

Then you can simply code your method as usual:


case NSFetchedResultsChangeUpdate:
{
if (newIndexPath == nil)
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
else
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];

[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
break;
}


Download the SafeFetchedResultsController class and test project.

 

13 comments:

ecstasy2 said...

Hi Guy.
your post is almost a life saver one.
it solved one of the problems i'm having my app is still crashing.

The problem is that i've been using NSFetchedResultsController everywhere in my app and was working on the 3.1.2 version of the SDK, now my client say that i should make it compatible with the 3.0 version and the deadline is almost there.

everytime i change an object handled by the contoller, the application is crashing with very weird errors:

2010-03-13 19:29:08.804 Instaproofs[2292:207] Serious application error. Exception was caught during Core Data change processing: *** -[NSCFArray objectAtIndex:]: index (1) beyond bounds (1) with userInfo (null)
2010-03-13 19:29:08.807 Instaproofs[2292:207] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSCFArray objectAtIndex:]: index (1) beyond bounds (1)'
2010-03-13 19:29:08.809 Instaproofs[2292:207] Stack: (
807902715,
7364425,
807986683,
807986522,
810976489,
810572359,
815058339,
815007323,
211374,
4363331,
810589786,
807635429,
810579728,
3620573,
3620227,
3614682,
3699752,
31187,
810595174,
807686849,
807683624,
839142449,
839142646,
814752238,
9912,
9766
)


i'm a dead man if this is not sorted out quickly so i would realy appreciate if you share with me your knowledge of the various bugs NSfetchedResultsController has in the 3.0 version of the SDK as it is working well on the 3.1 and above versions.

Regards.

Robbie Hanson said...

I believe there is a DEBUG define at the top of SafeFetchedResultsController. Can you set it to YES, crash the app, and then post the output. I found and fixed another bug from NSFRC the other day. Perhaps it's related.

davidblythe said...

Hi. I've had a devil of a time with the NSFRC and was very happy to have found your subclass. I've repackaged it as an adapter, rather than a subclass, to sit between the offending NSFRC and my true delegate. I've got other code that gets too tangled having to deal with a subclass, and the adapter lets me sidestep those issues nicely.

The code in your download does take care of several issues, but I am still having some trouble, and maybe that relates to your later comment on Mar 14 about having found other bugs in NSFRC.

One InconsistencyError that still occurs using your code is this: I have a table with two sections. I can edit a cell in the second section such that it should cause a new section 0 to be created with this cell as its sole row.

In that case, I'd expect to see a section insert, but NSFRC only sends a row move from--in this case--1,22 to 0,0. While technically true, it's missing the section insertion and the table still throws the exception.

Is this similar to the bug you found more recently? I see no more recent version of your code. Have you updated it?

Thanks for the good work! It's hard to see how Apple could let such a critical chunk of code needed for CoreData and tables remain so buggy.

Michael said...

Looks like this bug is still in iOS 4.3.3. I've been battling it for the last 2 weeks until I came across your fix. Thanks so much. It seems to be working 100% now!

Anonymous said...

Stil in ios 6!

Ridgewood Monahan said...

Very nicely done!!!

Ridgewood Monahan said...

Very nicely done!!! Your code worked like a charm!

Anonymous said...

The download of the zip file is no longer working. Could you maybe make it available again?

Anonymous said...

Thank you so much! I spent days trying to figure this out and it fixed it immediately.

Anonymous said...

Couldn't download zip. Could you make it available?

Anonymous said...

Couldn't download zip. Could you make it available?

Anonymous said...

Couldn't download zip, could you make it available again?

Anonymous said...

Thank you very much,
copying the code was all I had to do!