views:

662

answers:

3

I have a managed object with a dueDate attribute. Instead of displaying using some ugly date string as the section headers of my UITableView I created a transient attribute called "category" and defined it like so:

- (NSString*)category
{
    [self willAccessValueForKey:@"category"];

    NSString* categoryName;
    if ([self isOverdue])
    {
        categoryName = @"Overdue";
    }
    else if ([self.finishedDate != nil])
    {
        categoryName = @"Done";
    }
    else
    {
        categoryName = @"In Progress";
    }

    [self didAccessValueForKey:@"category"];
    return categoryName;
}

Here is the NSFetchedResultsController set up:

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Task"
                                          inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];

NSMutableArray* descriptors = [[NSMutableArray alloc] init];
NSSortDescriptor *dueDateDescriptor = [[NSSortDescriptor alloc] initWithKey:@"dueDate"
                                                                  ascending:YES];
[descriptors addObject:dueDateDescriptor];
[dueDateDescriptor release];
[fetchRequest setSortDescriptors:descriptors];

fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:@"category" cacheName:@"Root"];

The table initially displays fine, showing the unfinished items whose dueDate has not passed in a section titled "In Progress". Now, the user can tap a row in the table view which pushes a new details view onto the navigation stack. In this new view the user can tap a button to indicate that the item is now "Done". Here is the handler for the button (self.task is the managed object):

- (void)taskDoneButtonTapped
{
    self.task.finishedDate = [NSDate date];
}

As soon as the value of the "finishedDate" attribute changes I'm hit with this exception:

2010-03-18 23:29:52.476 MyApp[1637:207] Serious application error.  Exception was caught during Core Data change processing: no section named 'Done' found with userInfo (null)
2010-03-18 23:29:52.477 MyApp[1637:207] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no section named 'Done' found'

I've managed to figure out that the UITableView that is currently hidden by the new details view is trying to update its rows and sections because the NSFetchedResultsController was notified that something changed in the data set. Here's my table update code (copied from either the Core Data Recipes sample or the CoreBooks sample -- I can't remember which):

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

- (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:
            [self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            // Reloading the section inserts a new row and ensures that titles are updated appropriately.
            [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type)
    {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

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

I put breakpoints in each of these functions and found that only controllerWillChange is called. The exception is thrown before either controller:didChangeObject:atIndexPath:forChangeType:newIndex or controller:didChangeSection:atIndex:forChangeType are called.

At this point I'm stuck. If I change my sectionNameKeyPath to just "dueDate" then everything works fine. I think that's because the dueDate attribute never changes whereas the category will be different when read back after the finishedDate attribute changes.

Please help!

UPDATE:

Here is my UITableViewDataSource code:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[self.fetchedResultsController sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo numberOfObjects];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    [self configureCell:cell atIndexPath:indexPath];    

    return cell;
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];    
    return [sectionInfo name];
}
A: 

Try removing all of your NSFetchedResultsController delegate methods, and add just this one:

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {

    [self.tableView reloadData];

}

This should be enough to solve the issue you are experiencing.

unforgiven
Well this did fix the issue of the exception. However now I have a new problem. Calling reloadData on the UITableView doesn't end up changing the name of the section from "In Progress" to "Done". Putting a breakpoint in the "category" method verifies that the correct category string is returned. But for some reason the UITableView doesn't change the section title.
Mike H.
You also need to implement - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section; to correctly handle your section names when reloadData is called.
unforgiven
I do. I've updated my question with my UITableViewDataSource implementation. As you can see I'm just using the sectionInfo from the fetched results controller. From my breakpoints it appears that the correct category is returned but that the fetched results controller isn't updating its sections. Is there anything more I need to do to force the fetched results controller to update them?
Mike H.
I suspect that the problem is due to Core Data caching your transient attribute. Try passing nil as the cacheName argument in your initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName: method, or, simply delete the cache before instantiating your NSFetchedResultsController using the following method:+ (void)deleteCacheWithName:(NSString *)name;This should do the trick if the problem is due to caching.
unforgiven
Yeah, I tried specifying nil as the cacheName but it still didn't fix the problem. Ultimately I've given up on trying to make it work via a transient attribute. Instead I'm now saving the fetched results to an array which I then split into sections. This seems to work fine. I'm not going to mark this question as answered though since I was unable to find out the reason for the crash or why the section names did not update using your suggestions. Regardless, thanks for all your help!
Mike H.
+1  A: 

It looks to me like your problem lies with your "category" transient property that you are using to supply the sectionNameKeyPath. The sectionNameKeyPath must order the same as the primary sort descriptor. In your case, this means that all "Overdue" tasks MUST have dates earlier than all "Done" tasks MUST have dates earlier than all "In Progress" tasks. It is possible to construct a scenario where a "Done" task has a dueDate that comes after an "In Progress" task or comes before an "Overdue" task. This scenario breaks the ordering requirement of the sectionNameKeyPath and causes the NSFetchedResultsController to throw an NSInternalConsistencyException.

I propose a solution to your problem that doesn't involve rolling your own array which then must be split into sections. Create an integer attribute in your model where you map 0 to "Overdue", 1 to "Done", and 2 to "In Progress". Make this the primary sort descriptor in your NSFetchRequest and sort this property in ascending order. Add a secondary sort descriptor to the NSFetchRequest that sorts the dueDate property in ascending order. Modify your category method to derive category names from the integer attribute you created above and use that as your sectionNameKeyPath. You will need to update the integer attribute to update tasks as they move from in progress to overdue to done, etc.

glorifiedHacker
I respectfully disagree with Marcus - I don't think this is a bug at all. The documentation for NSFetchedResultsController states: "The fetch request must have at least one sort descriptor. If the controller generates sections, the first sort descriptor in the array is used to group the objects into sections; its key must either be the same as sectionNameKeyPath or the relative ordering using its key must match that using sectionNameKeyPath." As I see it, the crash is happening because the key for the sort descriptor and the key for the sectionNameKeyPath have different sort orderings.
glorifiedHacker
Very interesting suggestion. Makes sense based on what I've read from the SDK docs. I've already gone the route of having a separate array but I'll see if I can give this a try at some point. Thanks!
Mike H.
A: 

The crash is being caused by the NSFetchedResultsController not knowing about the "done" category before hand and therefore crashing. I have seen this crash a few other times in other questions and with each one I recommend submitting a radar ticket to Apple. This is a bug in the NSFetchedResultsController.

Marcus S. Zarra