Custom section name failed NSFetchedResultsController

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 this:

- (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's NSFetchedResultsController set:

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 looks fine at first, showing unfinished items whose due date has not passed in the section under the heading In Progress. The user can now push a row in the table view that pushes the new detail view onto the navigation stack. In this new view, the user can click a button to indicate that the item is now Done. Here is the button handler (self.task is a managed object):

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

      

As soon as the value of the "finishedDate" attribute changes, I got caught in 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 was able to figure out that the UITableView, which is currently hidden in the new details view, is trying to update its rows and sections because the NSFetchedResultsController has been notified that something has changed in the dataset. Here's my code for updating the table (copied from either the Core Data Recipes sample or the CoreBooks sample - I don't remember which one):

- (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 the controller WillChange is being called. An exception is thrown before any controller: didChangeObject: atIndexPath: forChangeType: newIndex or controller: didChangeSection: atIndex: forChangeType is called.

At this point, I am stuck. If I change my sectionNameKeyPath to just "dueDate" then everything is fine. I think that since the dueDate attribute never changes, whereas the category will be different when read after the finalDate 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];
}

      

+2


source to share


2 answers


The crash was caused by NSFetchedResultsController, not knowing about the "made" category before manual and therefore crashing. I've seen this crash several times on other questions, and with each one, I recommend sending a radar ticket to Apple. This is a bug in NSFetchedResultsController

.



+2


source


It seems to me that your problem lies in the category transient type property that you use to provide the sectionNameKeyPath. The NameKeyPath section must order the same as the main collation 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. You can build a scenario where the Finish task has a DueDate value that comes after the In Progress task or precedes the DueDate task. This script violates the ordering requirement for the NameKeyPath section and causes NSFetchedResultsController to throw an NSInternalConsistencyException.



I propose a solution to your problem that does not involve translating your own array, which then has to be split into sections. Create an integer attribute in your model where you press 0 for "Overdue", 1 for "Done" and 2 for "In Progress". Make this the main 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 the category method to get the category names from the integer attribute you created above and use that as yourNameKeyPath section. You will need to update the integer attribute to update the tasks as they go from process to overdue, etc.

+1


source







All Articles