0

in getting familiar with core data i have found myself puzzled by the question of what to pass various view controllers (VCs) when trying to add data.

for example, in the CoreDataRecipes project that apple provides as an example (http://developer.apple.com/library/ios/#samplecode/iPhoneCoreDataRecipes/Introduction/Intro.html) they use the following approach

when the user wants to add a recipe to the list of recipes presented in the master table view, and hits the Add button, the master table view controller (called RecipeListTableViewController) creates a new managed object (Recipe) as follows:

- (void)add:(id)sender {
 // To add a new recipe, create a RecipeAddViewController.  Present it as a modal view so that the user's focus is on the task of adding the recipe; wrap the controller in a navigation controller to provide a navigation bar for the Done and Save buttons (added by the RecipeAddViewController in its viewDidLoad method).
RecipeAddViewController *addController = [[RecipeAddViewController alloc] initWithNibName:@"RecipeAddView" bundle:nil];
addController.delegate = self;

Recipe *newRecipe = [NSEntityDescription insertNewObjectForEntityForName:@"Recipe" inManagedObjectContext:self.managedObjectContext];
addController.recipe = newRecipe;

UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController];
[self presentModalViewController:navigationController animated:YES];

[navigationController release];
[addController release];
}

this newly created object (a Recipe) is passed to the RecipeAddViewController. the RecipeAddViewController has two methods, save and cancel, as follows:

- (void)save {

recipe.name = nameTextField.text;

NSError *error = nil;
if (![recipe.managedObjectContext save:&error]) {
    /*
     Replace this implementation with code to handle the error appropriately.

     abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
     */
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}       

[self.delegate recipeAddViewController:self didAddRecipe:recipe];

}

- (void)cancel {

[recipe.managedObjectContext deleteObject:recipe];

NSError *error = nil;
if (![recipe.managedObjectContext save:&error]) {
    /*
     Replace this implementation with code to handle the error appropriately.

     abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
     */
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}       

[self.delegate recipeAddViewController:self didAddRecipe:nil];

}

i am puzzled about this design approach. why should the RecipeListViewController create the object before we know if the user wants to actually enter a new recipe name and save the new object? why not pass the managedObjectContext to the addRecipeController, and wait until the user hits save to create the object and populate its fields with data? this avoids having to delete the new object if there is no new recipe to add after all. or why not just pass a recipe name (a string) back and forth between the RecipeListViewController and the RecipeAddController?

i'm asking because i am struggling to understand when to pass strings between segues, when to pass objects, and when to pass managedObjectContexts...

any guidance much appreciated, incl. any links to a discussion of the design philosophies at issue.

dave adelson
  • 853
  • 9
  • 15

1 Answers1

1

Your problem is that NSManagedObjects can't live without a context. So if you don't add a Recipe to a context you have to save all attributes of that recipe in "regular" instance variables. And when the user taps save you create a Recipe out of these instance variables.

This is not a huge problem for an AddViewController, but what viewController do you want to use to edit a recipe? You can probably reuse your AddViewController. But if you save all data as instance variables it gets a bit ugly because first you have to get all data from the Recipe, save it to instance variables, and when you are done you have to do the reverse.

That's why I usually use a different approach. I use an editing context for editing (or adding, which is basically just editing).

- (void)presentRecipeEditorForRecipe:(MBRecipe *)recipe {
    NSManagedObjectContext *editingContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    editingContext.parentContext = self.managedObjectContext;
    MBRecipe *recipeForEditing;
    if (recipe) {
         // get same recipe inside of the editing context. 
        recipeForEditing = (MBRecipe *)[editingContext objectWithID:[recipe objectID]];
        NSParameterAssert(recipeForEditing);
    }    
    else {
        // no recipe for editing. create new one
        recipeForEditing = [MBRecipe insertInManagedObjectContext:editingContext];
    }

    // present editing view controller and set recipeForEditing and delegate
}

Pretty straight forward code. It creates a new children context which is used for editing. And gets a recipe for editing from that context.

You must not save the context in your EditViewController! Just set all desired attributes of Recipe, but leave the context alone.

After the user tapped "Cancel" or "Done" this delegate method is called. Which either saves the editingContext and our context or does nothing.

- (void)recipeEditViewController:(MBRecipeEditViewController *)editViewController didFinishWithSave:(BOOL)didSave {
    NSManagedObjectContext *editingContext = editViewController.managedObjectContext;
    if (didSave) {
        NSError *error;
        // save editingContext. this will put the changes into self.managedObjectContext
        if (![editingContext save:&error]) {
            NSLog(@"Couldn't save editing context %@", error);
            abort();
        }

        // save again to save changes to disk
        if (![self.managedObjectContext save:&error]) {
            NSLog(@"Couldn't save parent context %@", error);
            abort();
        }
    }
    else {
        // do nothing. the changes will disappear when the editingContext gets deallocated
    }
    [self dismissViewControllerAnimated:YES completion:nil];
    // reload your UI in `viewWillAppear:`
}
Matthias Bauch
  • 89,811
  • 20
  • 225
  • 247
  • thank for your response. this seems very elegant, and avoids what i was worried about, namely, that interruption of the addRecipe process could end up leaving an empty record in my managedObjectContext. i have several further questions regarding the approach. first, i have read up on the concurrency types and am curious why you choose NSMainQueueConcurrencyType for the editing context. second, why is NSParameterAssert(recipeForEditing); needed? – dave adelson Jul 16 '13 at 20:29
  • Update: when i try to use Matthias B.'s approach i get the following error at runtime: *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Parent NSManagedObjectContext must use either NSPrivateQueueConcurrencyType or NSMainQueueConcurrencyType.' ...this begs the question whether when i declare the original NSManagedObjectContext in the app delegate i need to make it one of these types, and if so, which is superior? – dave adelson Jul 20 '13 at 22:00