Cancel New CoreData Object? - iphone

In my app, I have my master view controller which displays all my coredata objects.
When the user adds an object this runs and the next detail view opens to enter details for the new object:
-(IBAction)addPerson:(id)sender
{
Person *p = (Person *)[NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:managedObjectContext];
PersonDetailViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"PersonDetail"];
vc.managedObjectContext = fetchedResultsController.managedObjectContext;
vc.person = p;
vc.isNewPerson = YES;
[self.navigationController pushViewController:vc animated:YES];
}
Now I have a delete button in the detail view which calls this:
[managedObjectContext deleteObject:person];
NSError *err;
if (![managedObjectContext save:&err])
{
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", err, [err userInfo]);
exit(-1); // Fail
}
This is how I currently cancel a new coredata object. However I am having issues with it.
How would you recommend I best cancel the creation of a new object? Create and delete, or never create it until confirmed? I'm unsure.

I'd actually never create the managed object until confirmed!
But in the first place I'd rewrite the PersonDetailViewController to not have a dependency to the Person entity at all (loose coupling). Instead I'd define properties in your PersonDetailViewController for the various attributes of the Person object you like to set/edit and then handle the creation of the Person managed object in a save delegate method or so. With this approach you could also cancel the creation use-case half through, without the need for creation of a new managed object etc.
For better illustration, the delegate method would look somewhat like this:
- (void) personDidSave: (PersonDetailViewController*) controller {
// Create a new Person object with the values from the controller
// and add persist it to core data
Person *p = (Person *)[NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:managedObjectContext];
p.name = controller.name;
p.surname= controller.surname;
// etc.
// save to core data
// Refresh the table or whatever view
}

Related

Core Data returns Null when i save

here is my code,
hand is the entity, and addRounds is a textField
-(IBAction)save{
[self dismissViewControllerAnimated:YES completion:nil];
[self.hand setValue:self.addRounds.text forKey:#"rounds"];
NSError *error;
if (![self.managedObjectContext save:&error]) {
//Handle Error
}
NSLog(#"%#", self.addRounds.text);
NSLog(#"%#", self.hand.rounds);
}
the console out puts
2012-11-25 16:51:18.847 App[3187:c07] 1
2012-11-25 16:51:18.848 App[3187:c07] (null)
so, for some reason, its not saving properly. Could anyone please help me! Thank You!
-(IBAction)save{
if (self.managedObjectContext == nil)
{
self.managedObjectContext = [(RootAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
Hand *hand = [NSEntityDescription insertNewObjectForEntityForName:#"Hand" inManagedObjectContext:self.managedObjectContext];
[self dismissViewControllerAnimated:YES completion:nil];
hand.rounds = nil;
self.managedObjectContext = self.app.managedObjectContext;
NSError *error;
if (![self.managedObjectContext save:&error]) {
//Handle Error
}
}
Edit
So Basically There is a total of 4 Views.
1) view with Table view, user can press '+' button
2) This view allows the user to add a cell to the table view
3) allows the user to edit the table view cells
4) this is a completely different view that also allows the user to edit the table view cells.
I'm using the code at the top of the question to save in both views 3 & 4. It works perfectly in view 3 but not 4!!
UPDATE!!!
So, I recoded the app so that views 1 & 4 are the only two views in the app. When i push view 2 and view 3 in between view 1 and 4 it sets my managedObjectContext's rounds attribute to null.
Is the rounds attribute of your hand entity a string in your model?
First, you should set the type on your hand entity's rounds attribute to a number type. 16bit integer would probably suffice (we're talking about a card game, right?), but you could make it bigger if you like.
Next change your code to:
-(IBAction)save{
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
NSNumber *rounds = [numberFormatter numberFromString:numberself.addRounds.text];
[self dismissViewControllerAnimated:YES completion:nil];
[self.hand setValue:rounds forKey:#"rounds"];
NSError *error;
if (![self.managedObjectContext save:&error]) {
//Handle Error
}
NSLog(#"%#", self.addRounds.text);
NSLog(#"%#", self.hand.rounds);
Also, I'm assuming you've inserted the instance of hand that you're setting properties on somewhere before this point. Maybe I shouldn't assume. Have you already initialized this managed object instance, self.hand, to point to data in the store?
For example, I like to use lazy instantiation:
- (Hand*)hand
{
if (_hand = nil) {
_hand = [NSEntityDescription insertNewObjectForEntityForName:#"Hand" inManagedObjectContext:self.managedObjectContext];
}
return _hand;
}
This assumes a purely -create oriented- design. In most cases, you'd want to edit an existing object and update its rounds count. For this, you should attempt to retrieve a Hand that you're editing first. You'd do that with an NSFetchRequest, and there a zillions of examples of that, so I won't repeat them here. If There were no matches, this getter would create one as a fall-back. Also, best practice is to create a category for Hand (maybe Hand+Edit.m) which contains methods for retrieving different Hands, creating them, and updating common properties.
I'd create worker methods inside the Hand object category like these:
+ (void)incrementRoundsOnHand:(Hand *)hand withManagedObjectContext:(NSManagedObjectContext *)context
+ (void)incrementRoundsBy:(NSUInteger)count onHand:(Hand *)hand withManagedObjectContext:(NSManagedObjectContext *)context
// or some sort of unique identifier, date, number, etc
+ (Hand *)handWithName:(NSString *)name withManagedObjectContext:(NSManagedObjectContext *)context
I'd then have handWithName: (or whatever) do an NSFetchRequest, and if nothing matches, create a new hand and return it. Either way, you get a hand back. Important is that you don't deal with manipulating the specifics of your Hand entity outside of the Hand managed object class. Note, since these are class methods, they can be called directly.

undo all changes made in the child view controller

There are two entities: Author and Book. Author has an attribute authorName and a to-many relationships books. Book has several attributes and a relationship author. There is a view controller (VCAuthor) to edit an Author object. The child view controller(VCBook) is to edit books of the author. There is only one managedobjectcontext. In the VCBook class i group the undomanager as following
-(void)viewDidLoad
{
NSUndoManager *anUndoManager = [[NSUndoManager alloc] init];
[self.book.managedObjectContext setUndoManager:anUndoManager];
[anUndoManager release];
[self.book.managedObjectContext.undoManager beginUndoGrouping];
}
-(void)cancelAction:(id)sender
{
NSLog(#"%#", self.author.authorName);
[self.book.managedObjectContext.undoManager endUndoGrouping];
[self.book.managedObjectContext.undoManager undoNestedGroup];
self.book.managedObjectContext.undoManager = nil;
NSLog(#"%#", self.author.authorName);
[self dismissModalViewControllerAnimated:YES];
}
the cancelAction is linked to an cancel button on the VCBook which used to undo all the changes made in the VCBook.
Problems is here: First, in the VCAuthor, I edit the authorName in an UITextfiled authorNameTextField from Obama to Big Obama, and save it to the MOC by author.authorName = authorNameTextField.text in the - (void)viewWillDisappear:(BOOL)animated{} method. Then I went into the child view controller VCBook to edit books of the author and click the cancel button to get back to the VCAuthor. I find the authorName still be Obama, that means the expected change of the authorName has been undo. The change of the authorName is not in the undo group at all, and why could this happen? ps. of course i reloadData when i get back into VCAuthor.
I just NSLog the authorName before and after the undo. Before undo the authorName is the changed one Big Obama, and after undo it become Obama
Several things to consider. First, in a scenario like this, I would use a separate MOC instead of the undo manager. Namely, I'd do something like this (assuming ARC - you can do the mapping if necessary)...
You must have some other code providing the book to the VC through a setter, since you access it in viewDidLoad. I'd change viewDidLoad to something like this...
-(void)viewDidLoad
{
self.managedObjectContext = [[NSManagedObjectContext alloc] init];
self.managedObjectContext.parentContext = self.book.managedObjectContext;
// We are in the main thread, so we can safely access the main MOC
// Basically, move our object pointer to book into our local MOC.
NSError * error = nil;
Book *book = [self.managedObjectContext existingObjectWithID:self.book.objectID error:&error];
// handle nil return and/or error
self.book = book;
// Now, your access to book will be in the local MOC, and any changes
// you make to the book or book.author will all be confined to the local MOC.
}
Now, all you have to do is call
[self.managedObjectContext save:&error];
in your saveAndDismiss action. If you don't call save, none of the changes will be saved, they will all just automatically disappear.
EDIT
Note, that the above "save" just moves the object state into the parent context. So, the "main" MOC now has the changes from the "child" but none of the changes have been saved to disk yet.

Core Data Persistent Store Coordinator NSURL error

I've been working on an app that takes user ratings about mood. I am using Core Data to store this data. Initially, I am trying to store ratings and strings of "accomplishments". I've set up an entity in Core Data called "Day" with attributes date (of type Date), dailyRating (of type Int16), dailyAccomp (of type String), and dailyAccompRating (of type Int16). My app crashes within my app delegate's Core Data persistentStoreCoordinator method at the following stmnt:
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"SelfEsteemBldr.sqlite"];
The error that I'm given is
[NSPathStore2 URLByAppendingPathComponent:]: unrecognized selector sent to instance 0x4d6f440.
Maybe a little background about how the error comes about might be helpful.
My Main window has a tab bar controller as the rootViewController. Within the tab for the CD model (LogViewController), I've set up a tableView Controller within a navigation controller. The nav bar has an add button, that pushes a new view that basically has textfields so the user can enter the relevant data. Within that view, there is a nav bar with a Done button. When the user is done, the Done button changes to a Save button. When I tap the Save button, the app crashes. The Save button is a UIButtonItem within ViewDidLoad. Here's the code for the save button:
UIBarButtonItem *newSaveButton =
[[UIBarButtonItem alloc]
initWithTitle:NSLocalizedString(#"Save", nil)
style:UIBarButtonItemStylePlain
target:self
action:#selector(performAddNewDay:)];
self.saveButton = newSaveButton;
[newSaveButton release];
The performAddNewDay method within the UIButtonItem looks like this:
- (void) performAddNewDay:(id)paramSender{
SelfEsteemBldrAppDelegate *delegate = (SelfEsteemBldrAppDelegate *)
[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = delegate.managedObjectContext;
NSLog(#"Past init point in PerformAddNewDay");
// Get the values from the text fields
NSInteger dailyRatingAsInteger = [self.textFieldDailyRating.text integerValue];
NSNumber *ddailyRating = [NSNumber numberWithInteger:dailyRatingAsInteger];
NSLog(#"Daily Rating Entered is %#", ddailyRating);
NSString *ddailyAccomplishment = self.textFieldAccomplishment.text;
NSLog(#"Daily Accomplishment Entered is %#", ddailyAccomplishment);
NSInteger dailyAccompRatingAsInteger = [self.textFieldAccomplishmentRating.text integerValue];
NSNumber *ddailyAccompRating = [NSNumber numberWithInteger:dailyAccompRatingAsInteger];
NSLog(#"Daily Accomp Rating Entered is %#", ddailyAccompRating);
// Create a new instance of Day
Day *newDay = [NSEntityDescription
insertNewObjectForEntityForName:#"Day"
inManagedObjectContext:context];
if (newDay != nil){
// Set the properties according to the values we retrieved from the text fields
newDay.dailyAccomp = ddailyAccomplishment;
newDay.dailyRating = ddailyRating;
newDay.dailyAccompRating = ddailyAccompRating;
NSError *savingError = nil;
// Save the new day
if ([context save:&savingError] == YES){
// If successful, simply go back to the previous screen
[self.navigationController popViewControllerAnimated:YES];
} else {
// If we failed to save, display a message
[self
displayAlertWithTitle:NSLocalizedString(#"Saving", nil)
message:NSLocalizedString(#"Failed to save the context", nil)];
}
} else {
// We could not insert a new Day managed object
[self
displayAlertWithTitle:NSLocalizedString(#"New Day", nil)
message:NSLocalizedString(#"Failed To Insert A New Day", nil)];
}
}
I've commented out most of the code to try to find the offending statement, and it seems to be
**NSManagedObjectContext *context = delegate.managedObjectContext;**
That is, if I comment everything below and including this stmnt, app doesn't crash. It doesn't do anything, it just "waits" (as expected). If I uncomment this stmnt, app crashes. SelfEsteemBldrAppDelegate is also imported using the #import "SelfEsteemBldrAppDelegate.h' stmnt.
Again, the error that I'm getting is in the Core Data Stack persistentStoreCoordinator method within SelfEsteemBldrAppDelegate.m. The crash occurs at the following:
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"SelfEsteemBldr.sqlite"];
The error that I'm given is
[NSPathStore2 URLByAppendingPathComponent:]: unrecognized selector sent to instance 0x4d6f440
So, after all that, any ideas as to why I might be getting this message, and what I can do to resolve it? From what I understand, I'm not supposed to interact with the Core Data Stack methods, so I don't want to fool around with that code. Any help would be greatly appreciated. Also, if I've left out any info you may need, please let me know. Thanks.
Normally, I faced this problem and encountered that this error comes when managedObjectContext is nil... To solve this i have used below code and worked fine for me..
if (managedObjectContext == nil) {
managedObjectContext = [(iBountyHunterAppDelegate *) [[UIApplication sharedApplication] delegate] managedObjectContext];
}

NSInvalidArgumentException: Illegal attempt to establish a relationship between objects in different contexts

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?
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.
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.

Deleting Core Data object before popping a view from the UINavigationController

The following code deletes a core data object if the user did not end up modifying the new entry (this happens on an insert operation). The root view controller created the entity and passed a reference to my view. It works fine if the user hits a "done" button on my view, but if instead they navigate backwards using the "back" button on the nav bar, the root view it returns to just hangs forever.
Am I doing something obviously wrong? I have considered waiting to create the entity until the user is finished with the view, but in the future this view will also handle editing existing entities, so my current method of passing an existing entity to this view is preferred.
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// If the baby has no name, delete the Baby object that the root view
// already created. Otherwise save it.
NSManagedObjectContext *context = self.baby.managedObjectContext;
if ( [self.babyNameField.text length] == 0 )
[context deleteObject:baby];
// Save
NSError *error = nil;
if (![context save:&error]) {
// unresolved jmu - handle error
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
This is a common approach and this code looks fine.
Have you set a breakpoint on it and walked through to make sure? You need to debug and track down where the code is hanging.