Monday, March 29, 2010

My Last Word on NSFetchedResultsController

Somehow, I never posted my final fix for the various NSFetchedResultsController problems. I submitted a bug report to Apple to get the actual row counts out of NSFetchedResultsController including any deferred inserts and deletes, but went ahead with developing a workaround that keeps track of the inserts and deletes locally. I thought I had posted this information, but I guess I never did (Sorry!). I am optimistic that these issues will be fixed in 3.2, but haven't had time to really run the new version of NSFetchedResultsController through its paces, and even if I had, the NDA would keep me from being able to tell you what I had found. Nevertheless, the fact that it has been updated makes me optimistic that this workaround is temporary and that Apple has finally shipped a production-ready version of NSFetchedResultsController. In the meantime…

I'm going to give the changes in context of the Chapter 7 application from More iPhone 3 Development, but these changes are generic and could just be copied to any project using an NSFetchedResultsController and the delegate methods from Chapter 2. I don't usually encourage copy-and-paste coding, but for a temporary workaround it makes sense.

In HeroListViewController.h, we have to add an instance variable to keep track of the deferred inserts and deletes:

#import <UIKit/UIKit.h>

#define kSelectedTabDefaultsKey @"Selected Tab"
enum {
kByName = 0,
kBySecretIdentity,
}
;
@class ManagedObjectEditor;
@interface HeroListViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, UITabBarDelegate, UIAlertViewDelegate, NSFetchedResultsControllerDelegate>{

UITableView *tableView;
UITabBar *tabBar;
ManagedObjectEditor *detailController;

@private
NSFetchedResultsController *_fetchedResultsController;
NSUInteger sectionInsertCount;
}

@property (nonatomic, retain) IBOutlet UITableView *tableView;
@property (nonatomic, retain) IBOutlet UITabBar *tabBar;
@property (nonatomic, retain) IBOutlet ManagedObjectEditor *detailController;
@property (nonatomic, readonly) NSFetchedResultsController *fetchedResultsController;
- (void)addHero;
- (IBAction)toggleEdit;
@end



Then, we need to make some changes to two of the NSFetchedResultsController delegate methods. Easiest thing is to probably just replace the existing version with these new versions. The only difference from the previous version is that we keep track of insertions and deletions for each transaction and then use the row count for sections is determined by adding the deferred insertion / deletion count to the number reported by the NSFetchedResultsController:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
sectionInsertCount = 0;
[self.tableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate: {
NSString *sectionKeyPath = [controller sectionNameKeyPath];
if (sectionKeyPath == nil)
break;
NSManagedObject *changedObject = [controller objectAtIndexPath:indexPath];
NSArray *keyParts = [sectionKeyPath componentsSeparatedByString:@"."];
id currentKeyValue = [changedObject valueForKeyPath:sectionKeyPath];
for (int i = 0; i < [keyParts count] - 1; i++) {
NSString *onePart = [keyParts objectAtIndex:i];
changedObject = [changedObject valueForKey:onePart];
}

sectionKeyPath = [keyParts lastObject];
NSDictionary *committedValues = [changedObject committedValuesForKeys:nil];

if ([[committedValues valueForKeyPath:sectionKeyPath] isEqual:currentKeyValue])
break;

NSUInteger tableSectionCount = [self.tableView numberOfSections];
NSUInteger frcSectionCount = [[controller sections] count];
if (tableSectionCount + sectionInsertCount != frcSectionCount) {
// Need to insert a section
NSArray *sections = controller.sections;
NSInteger newSectionLocation = -1;
for (id oneSection in sections) {
NSString *sectionName = [oneSection name];
if ([currentKeyValue isEqual:sectionName]) {
newSectionLocation = [sections indexOfObject:oneSection];
break;
}

}

if (newSectionLocation == -1)
return; // uh oh

if (!((newSectionLocation == 0) && (tableSectionCount == 1) && ([self.tableView numberOfRowsInSection:0] == 0))) {
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:newSectionLocation] withRowAnimation:UITableViewRowAnimationFade];
sectionInsertCount++;
}


NSUInteger indices[2] = {newSectionLocation, 0};
newIndexPath = [[[NSIndexPath alloc] initWithIndexes:indices length:2] autorelease];
}

}

case NSFetchedResultsChangeMove:
if (newIndexPath != nil) {

NSUInteger tableSectionCount = [self.tableView numberOfSections];
NSUInteger frcSectionCount = [[controller sections] count];
if (frcSectionCount != tableSectionCount + sectionInsertCount) {
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:[newIndexPath section]] withRowAnimation:UITableViewRowAnimationNone];
sectionInsertCount++;
}



[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath]
withRowAnimation: UITableViewRowAnimationRight
]
;

}

else {
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:[indexPath section]] withRowAnimation:UITableViewRowAnimationFade];
}

break;
default:
break;
}

}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {

case NSFetchedResultsChangeInsert:
if (!((sectionIndex == 0) && ([self.tableView numberOfSections] == 1))) {
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
sectionInsertCount++;
}


break;
case NSFetchedResultsChangeDelete:
if (!((sectionIndex == 0) && ([self.tableView numberOfSections] == 1) )) {
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
sectionInsertCount--;
}

break;
case NSFetchedResultsChangeMove:
break;
case NSFetchedResultsChangeUpdate:
break;
default:
break;
}

}




You can find the fixed version of the Chapter 7 application right here.



15 comments:

AC said...

I could have sworn you have already posted this. I could easily check the code if I was at home. I have been spending the past weeks combing through your blog. However, I might be able to find it amongst the many links I have been bookmarking.

This is my first post and I would be remiss if I did not say that I have thoroughly enjoyed and benefited from your 2 books "Beginning..." and "More...". If a book has your name on it, I will need a really good reason to NOT buy the book. You produce quality work and I very much appreciate it.

AC said...

http://iphonedevelopment.blogspot.com/2010_03_01_archive.html

I did not do a detailed comparison (have to get back to my day job), but the above post seems to be addressing the same thing?

Jeff LaMarche said...

AC:

I could have sworn I already post this, however the link you in that comment is to the exact same post (March 2010 archive).

It is possible I posted this, but I couldn't find it. Possibly I accidentally deleted it or something. Either way, I've had enough people ask about this that it was worth reposting.

Thanks for the kind words, by the way.

Jeff

Andy W said...

Isn’t this it?

http://iphonedevelopment.blogspot.com/2010/01/chapter-4-and-tale-of.html

I was sure I had seen it before, and had made the appropriate amends to my version as I worked through the book.

Unless there’s a really subtle difference, seems to be the same code.

It looks like the chapter number varies though, which may be why you lost it.

Andy W

Ghulam Mustafa said...

Small question: Have you deliberately left out "break;" from NSFetchedResultsChangeUpdate case? if so, can you please explain why?

Greg said...

I've got the same question about the missing break after NSFetchedResultsChangeUpdate.

Also, I've got a more general question about validation: is there a way to avoid having all of the validators called when saving a ManagedObjectContext? It seems kind of silly to have every validator for every attribute execute every single time an individual attribute is being saved.

Am I just missing something? I've dug through the Apple docs on CoreData, but haven't found anything really enlightening.

Ghulam Mustafa said...

While my last question is still pending, I've ran into a new problem.

My application quits with these as it's last words:

*** LOG START ***
2010-04-08 19:08:36.080 MyProject[25244:207] -[MyEntityListViewController controllerWillChangeContent:], called
2010-04-08 19:08:36.081 MyProject[25244:207] -[MyEntityListViewController controller:didChangeSection:atIndex:forChangeType:], NSFetchedResultsChangeInsert
2010-04-08 19:08:48.575 MyProject[25244:207] -[MyEntityListViewController controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:], NSFetchedResultsChangeInsert
2010-04-08 19:08:48.576 MyProject[25244:207] -[MyEntityListViewController controllerDidChangeContent:], called
2010-04-08 19:08:48.577 MyProject[25244:207] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-984.38/UITableView.m:772
2010-04-08 19:08:48.578 MyProject[25244:207] Serious application error. Exception was caught during Core Data change processing: Invalid update: invalid number of sections. The number of sections contained in the table view after the update (2) 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). with userInfo (null)
2010-04-08 19:08:48.578 MyProject[25244:207] *** 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 (2) 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).'
*** LOG END ***

** DEBUG LOG START ***
#0 0x020e6004 in ___TERMINATING_DUE_TO_UNCAUGHT_EXCEPTION___
#1 0x946e3509 in objc_exception_throw
#2 0x01c06a85 in -[NSManagedObjectContext(_NSInternalChangeProcessing) _processRecentChanges:]
#3 0x01c43b3a in -[NSManagedObjectContext processPendingChanges]
#4 0x01c34957 in _performRunLoopAction
#5 0x0207f252 in __CFRunLoopDoObservers
#6 0x0207e65f in CFRunLoopRunSpecific
#7 0x0207dc48 in CFRunLoopRunInMode
#8 0x0286a615 in GSEventRunModal
#9 0x0286a6da in GSEventRun
#10 0x0031dfaf in UIApplicationMain
#11 0x00011612 in main at main.m:15
** DEBUG LOG END ***

What am i doing? I'm showing the table with sections. Records are brought by NSFetchedResultsController, and are sorted by "attributeSection". Table cells show "attributeRow".

Steps to reproduce:
1.) Empty Core Data
2.) I add a new record. It creates a new section and adds a new row. Now i have 1 section and 1 row at index path [0, 0]
3.) I add another record, such that the record adds a new row in a new section. Section should be added at index 0. Application crashes here.

S.O.S!

Ghulam Mustafa said...

I would really appreciate if someone can explain this code.

Greg said...

Jeff,

I've posted a question on the "More iPhone Dev" board. I'm hoping you can help. I'm trying to put a form on a panel that is not accessed via a Core Data driven table. I've tried several variations on the code you supply in the book, but I keep hitting problems. Do you think you could take a look at what I've posted? It's here: http://iphonedevbook.com/forum/viewtopic.php?f=65&t=4118&sid=196fc8f9b8df5b46f636b31a4ba40eca

If you've got an example using your ManagedObjectEditor code, that would be freakin' brilliant. If not, but have some hints, that would be just as good. If you'd rather I just leave you alone, well ... okay!

Greg said...

Ghulam,

I've seen that type of error, too. But it can be inconsistent. You might try to run this under SDK 3.2, as there are supposed to be some fixes to the Core Data internals.

Carb said...

I confirm that Jeff's code is flawed.
(Got other issues involving deleting section #0 while having more than one)
Not blaming you Jeff, Apple's fault.

I also confirm Apple fixed em in OS >3.2
Damn, got an app relying on NSFetchedResultController...

This bug has almost 1 year now since OS3 release...
And I won't talk of WiFi issues introduced with OS3...

Who said "we want to provide the most advanced and innovative platform to our developers" ? :p

Steven Hugg said...

Good stuff (better at least, than what I could come up with :) ). Recent changes fixed a section bug I'd been struggling with.

Only bug I found was that this didn't work for tables with one section. I had to add a line here:

case NSFetchedResultsChangeUpdate: {
NSString *sectionKeyPath = [controller sectionNameKeyPath];
if (sectionKeyPath == nil)
{
// needed to update rows
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
//
break;
}

soch said...

Jeff,

There are so many updates to the SuperDB app that it is difficult to find the last word on it.

I recently bought the "More... " book and was trying to find the code related to your blog post which shows sections in the table "SuperDB Core Data App with Sections" (http://iphonedevelopment.blogspot.com/2009/12/superdb-core-data-app-with-sections.html). But the project posted on that blog doesn't show sections. May be I am missing something basic.

Please help.

soch said...

I commented out the check in the didChangeSection delegate method as follows:
//if (!(sectionIndex == 0 && [self.tableView numberOfSections] == 1) &&
// [self.tableView numberOfRowsInSection:0] == 0)

And things are working smoothly in my app having table view with sections. The app has 2 tabs. A data entry tab which creates the DB objects, one at a time. And another which displays all the data in a uitableview with sections.

Also, I am trying out the Apple sample code DateSectionTitles (http://developer.apple.com/iphone/library/samplecode/DateSectionTitles/Introduction/Intro.html#//apple_ref/doc/uid/DTS40009939-Intro-DontLinkElementID_2).

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