Reputation: 226
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?
Upvotes: 4
Views: 4997
Reputation: 2741
Yeah, you definitely don't want to cross context boundaries when setting relationships between objects; they both need to be in the same NSManagedObjectContext. In the old EOF, there were APIs for faulting objects into different contexts, but doesn't look like CoreData has an equivalent.
Upvotes: 0
Reputation: 46718
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.
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.
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.
Upvotes: 6