views:

370

answers:

1

I have an app based on the CoreDataBooks example that uses an addingManagedObjectContext to add an Ingredient to a Cocktail in order to undo the entire add. The CocktailsDetailViewController in turn calls a BrandPickerViewController to (optionally) set a brand name for a given ingredient. Cocktail, Ingredient and Brand are all NSManagedObjects. Cocktail requires at least one Ingredient (baseLiquor) to be set, so I create it when the Cocktail is created.

If I add the Cocktail in CocktailsAddViewController : CocktailsDetailViewController (merging into the Cocktail managed object context on save) without setting baseLiquor.brand, then it works to set the Brand from a picker (also stored in the Cocktails managed context) later from the CocktailsDetailViewController.

However, if I try to set baseLiquor.brand in CocktailsAddViewController, I get:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a relationship 'brand' between objects in different contexts'

From this question I understand that the issue is that Brand is stored in the app's managedObjectContext and the newly added Ingredient and Cocktail are stored in addingManagedObjectContext, and that passing the ObjectID instead would avoid the crash.

What I don't get is how to implement the picker generically so that all of the Ingredients (baseLiquor, mixer, garnish, etc.) can be set during the add, as well as one-by-one from the CocktailsDetailViewController after the Cocktail has been created. In other words, following the CoreDataBooks example, where and when would the ObjectID be turned into the NSManagedObject from the parent MOC in both add and edit cases? -IPD

UPDATE - Here's the add method:

- (IBAction)addCocktail:(id)sender {

    CocktailsAddViewController *addViewController = [[CocktailsAddViewController alloc] init];
    addViewController.title = @"Add Cocktail";
    addViewController.delegate = self;

    // Create a new managed object context for the new book -- set its persistent store coordinator to the same as that from the fetched results controller's context.
    NSManagedObjectContext *addingContext = [[NSManagedObjectContext alloc] init];
    self.addingManagedObjectContext = addingContext;
    [addingContext release];

    [addingManagedObjectContext setPersistentStoreCoordinator:[[fetchedResultsController managedObjectContext] persistentStoreCoordinator]];

    Cocktail *newCocktail = (Cocktail *)[NSEntityDescription insertNewObjectForEntityForName:@"Cocktail" inManagedObjectContext:self.addingManagedObjectContext];
    newCocktail.baseLiquor = (Ingredient *)[NSEntityDescription insertNewObjectForEntityForName:@"Ingredient" inManagedObjectContext:self.addingManagedObjectContext];
    newCocktail.mixer = (Ingredient *)[NSEntityDescription insertNewObjectForEntityForName:@"Ingredient" inManagedObjectContext:self.addingManagedObjectContext];
    newCocktail.volume = [NSNumber numberWithInt:0];
    addViewController.cocktail = newCocktail;

    UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:addViewController];

    [self.navigationController presentModalViewController:navController animated:YES];

    [addViewController release];
    [navController release];

}

and here's the site of the crash in the Brand picker (this NSFetchedResultsController is backed by the app delegate's managed object context:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    cell.accessoryType = UITableViewCellAccessoryCheckmark;

    if ([delegate respondsToSelector:@selector(pickerViewController:didFinishWithBrand:forKeyPath:)]) 
    {
        [delegate pickerViewController:self 
               didFinishWithBrand:(Brand *)[fetchedResultsController objectAtIndexPath:indexPath] 
                            forKeyPath:keyPath]; // 'keyPath' is @"baseLiquor.brand" in the crash
    }
}

and finally the delegate implementation:

- (void)pickerViewController:(IngredientsPickerViewController *)pickerViewController
          didFinishWithBrand:(Brand *)baseEntity
                  forKeyPath:(NSString *)keyPath
{

    // set entity
    [cocktail setValue:ingredient forKeyPath:keyPath];  

    // Save the changes.
    NSError *error;
    if (![cocktail.managedObjectContext save:&error]) {
        // Update to handle the error appropriately.
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        exit(-1);  // Fail
    }

    // dismiss picker
    [self.navigationController popViewControllerAnimated:YES]
}

EVEN MORE

I'm making progess based on Marcus' suggestions -- I mapped the addingManagedObjectContexts to the parent managedObjectContext and wrapped everything in begin/endUndoGrouping to handle cancel vs. save.

However, the object to be created is in an NSFetchedResultsController, so when the user hits the "+" button to add the Cocktail, the (possibly-to-be-undone) entity briefly appears in the table view as the modal add view controller is presented. The MDN example is Mac-based so it doesn't touch on this UI behavior. What can I do to avoid this?

+1  A: 

Sounds like you are creating two different Core Data stacks (NSManagedObjectContext, NSManagedObjectModel, and NSPersistentStoreCoordinator). What you want to do from the example is just create two NSManagedObjectContext instances pointing at the same NSPersistentStoreCoordinator. That will resolve this issue.

Think of the NSManagedObjectContext as a scratch pad. You can have as many as you want and if you throw it away before saving it, the data contained within it is gone. But they all save to the same place.

update

The CoreDataBooks is unfortunately a really terrible example. However, for your issue, I would suggest removing the creation of the additional context and see if the error occurs. Based on the code you posted and I assume the code you copied directly from Apple's example, the double context, while virtually useless, should work fine. Therefore I suspect there is something else at play.

Try using a single context and see if the issue persists. You may have some other interesting but subtle error that is giving you this error; perhaps a overrelease somewhere or something along those lines. But the first step is to remove the double context and see what happens.

update 2

If it is crashing even with a single MOC then your issue has nothing to do with the contexts. What is the error you are getting with a single MOC? When we solve that, then we will solve your entire issue.

As for a better solution, use NSUndoManager instead. That is what it is designed for. Apple REALLY should not be recommending multiple MOCs in their example.

I answered a question on here recently about using the NSUndoManager with Core Data but you can also look at some of my articles on the MDN for an example.

Marcus S. Zarra
Don't think that's it -- both `managedObjectContext` and `addingManagedObjectContext` are set to the same `NSPersistentStoreCoordinator` as in the SDK example.
iPhoneDollaraire
The error you posted is caused by one situation; two `NSPersistentStoreCoordinator` instances. Perhaps if you shared your code that builds the contexts it would help to show what you are doing.
Marcus S. Zarra
Still stumped. There is only one `NSPersistentStoreCoordinator` (alloced and owned by the app delegate), a master `NSManagedObjectContext` (alloced and owned by the app delegate) and a couple `addingManagedObjectContexts` that are alloced in a `NSFetchedResultsController` and inited with the app delegate's `persistentStoreCoordinator`. The crash comes when a `Brand` already saved in the master MOC is assigned to a new `Ingredient` created in the adding MOC but not yet saved to the master. Which of the MOC building calls do you want to see?
iPhoneDollaraire
The creation of the secondary Contexts would be helpful
Marcus S. Zarra
Added to the question -- thanks for your help!
iPhoneDollaraire
Saw your update -- I already tried that after your first answer and it crashed harder (something to do with the interaction of the `NSNotificationCenter` and the fetched results controller inherited from Apple's example.)Can you recommend an alternate sample/tutorial to CoreDataBooks that preserves the undo-an-entire-add behavior?
iPhoneDollaraire
+1 especially because I can't believe this answer didn't get any votes before.
Shaggy Frog