views:

68

answers:

1

Hi. I've got a core data app with 2 views. The first view lists "Rooms", the second lists "Scenes" in rooms. The Rooms page has an edit NavItem button, which when pressed enables an add NavItem button. You can delete and add rooms from here. Added rooms simply appear with a default "New Room" name in the table. The second view is a list of Scenes in the selected room. Same deal here, you can delete and add Scenes, and added Scenes simply appear in the table with the name "New Scene". Nothing special really.

I'm using a FetchedResultsController in both view controllers, with the Scenes one having an NSPredicate to return only scenes from the selected room. I'm also using the controllerWillChangeContent, controllerDidChangeContent etc. delegate methods for the table view updates.

This all works fine at first, but usually after navigating around rooms and scenes then trying to delete a scene it will crash. It will inevitably crash if you play around with it long enough. It only happens when deleting a scene. If you press the edit button and delete a scene and it works, then all the following deletes in that edit session will always work. It will only ever crash on the first delete of the edit session.

The error I get is a strange one: * Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFType controllerWillChangeContent:]: unrecognized selector sent to instance 0x5e02d70'

The first part of this error sometimes changes. Sometimes it's __NSCFType, sometimes it's CALayer. This error only happens when deleting Scenes. Adding Scenes is fine 100%.

I've read another post on stackoverflow that suggests these sorts of errors can come from memory management issues. I've double checked the code, and also run it through Instruments with the leak instrument. There are no leaks.

Is there anything else I can check? Any ideas?

Here's the relevant code..

From ScenesTableViewController.m:

// used to show/hide the add button

- (void)setEditing:(BOOL)editing animated:(BOOL)animate
{
    [super setEditing:editing animated:animate];
    if(editing)
    {
        self.navigationItem.leftBarButtonItem = addButton;
    }
    else
    {
        self.navigationItem.leftBarButtonItem = nil;
    }
}

// called when the add button is pressed

- (void)addAction {
    NSEntityDescription *myContentEntity = [NSEntityDescription entityForName:@"Scene" inManagedObjectContext:managedObjectContext];
    Scene *contentToSave = [[Scene alloc] initWithEntity:myContentEntity insertIntoManagedObjectContext:managedObjectContext];
    [contentToSave setValue:@"New Scene" forKey:@"Name"];
    [parentRoom addRoomToScenesObject:contentToSave];

    NSError *error;
    if (![managedObjectContext save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        exit(-1);
    }
    [contentToSave release];
}

// delegate method that's being sent unrecognised selectors

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller is about to start sending change notifications, so prepare the table view for updates.
    [self.tableView beginUpdates];
}


// cell display code

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = nil; 
    cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 
    if (!cell) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
                                       reuseIdentifier:CellIdentifier] autorelease]; 
        [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator];
    }
    [self configureCell:cell atIndexPath:indexPath]; 
    return cell;
}

- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    NSManagedObject *mo = nil;
    NSString *temp = nil;
    mo = [fetchedResultsController objectAtIndexPath:indexPath];
    temp = [mo valueForKey:@"Name"];
    [[cell textLabel] setText:temp];
}

// cell editing code

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {

    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Delete the managed object at the given index path.
        [managedObjectContext deleteObject:[fetchedResultsController objectAtIndexPath:indexPath]];

        NSError *error;
        if (![managedObjectContext save:&error]) {
            // Update to handle the error appropriately.
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            exit(-1);  // Fail
        }
    }   
    else if (editingStyle == UITableViewCellEditingStyleInsert) {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
    }    
}

// NSFetchedResultsController code

- (NSFetchedResultsController *)fetchedResultsController {

    if (fetchedResultsController != nil) {
        return fetchedResultsController;
    }

    /*
     Set up the fetched results controller.
     */
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Scene" inManagedObjectContext:managedObjectContext];
    NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"Name" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:nameDescriptor, nil];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(SceneToRoom == %@)", parentRoom];

    [fetchRequest setSortDescriptors:sortDescriptors];   
    [fetchRequest setPredicate:predicate];
    [fetchRequest setEntity:entity];

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;

    [aFetchedResultsController release];
    [fetchRequest release];    
    [nameDescriptor release];
    [sortDescriptors release];

    return fetchedResultsController;
} 
+2  A: 

That error is most likely coming from a NSFetchedResultsController that has a released delegate. Do you have a UIViewController that you released and didn't release the associated NSFetchedResultsController?

Marcus S. Zarra
Thanks for the response Marcus. My NSFetchedResultsControllers are synthesized properties of their UITableViewControllers. In the viewDidUnload method I have self.fetchedResultsController = nil to relinquish ownership. Do I also need to release fetchedResultsController in dealloc as well?
Max Clarke
Yes, any property that is set as `retain` *must* be released in the `-dealloc`. In fact, I would **highly** recommend setting all properties to `nil` even if they are just `assign` in both methods.
Marcus S. Zarra
Thanks so much Marcus, that's fixed it. And thanks for the tip on retain'd properties and the -dealloc. Cheers!
Max Clarke
viewDidUnload is (unfortunately) not called from dealloc. It's also a good idea to do something like `self.fetchedResultsController.delegate = nil` in case something else is holding a reference to it.
tc.