NSUndoManager undo Not Working With Core Data - iphone

I am trying to create an iPhone application where the user can add entries. When he presses a new entry, a box will popup asking him for some information. Then he can either press "Cancel" or "Save" to discard the data or save it to disk.
For saving, I am using the Core Data framework, which works pretty well. However, I cannot get the "Cancel" button to work. When the window pops up, asking for information, I create a new object in the managed object context (MOC). Then when the user presses cancel, I try to use the NSUndoManager belonging to the MOC.
I would also like to do it using nested undo groups, because there might be nested groups.
To test this, I wrote a simple application. The application is just the "Window based application" template with Core Data enabled. For the Core Data model, I create a single entity called "Entity" with integer attribute "x". Then inside the applicationDidFinishLaunching, I add this code:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Override point for customization after app launch
unsigned int x=arc4random()%1000;
[self.managedObjectContext processPendingChanges];
[self.managedObjectContext.undoManager beginUndoGrouping];
NSManagedObject *entity=[NSEntityDescription insertNewObjectForEntityForName:#"Entity"
inManagedObjectContext:self.managedObjectContext];
[entity setValue:[NSNumber numberWithInt:x] forKey:#"x"];
NSLog(#"Insert Value %d",x);
[self.managedObjectContext processPendingChanges];
[self.managedObjectContext.undoManager endUndoGrouping];
[self.managedObjectContext.undoManager undoNestedGroup];
NSFetchRequest *fetchRequest=[[NSFetchRequest alloc] init];
NSEntityDescription *entityEntity=[NSEntityDescription entityForName:#"Entity"
inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entityEntity];
NSArray *result=[self.managedObjectContext executeFetchRequest:fetchRequest error:nil];
for(entity in result) {
NSLog(#"FETCHED ENTITY %d",[[entity valueForKey:#"x"] intValue]);
}
[window makeKeyAndVisible];
}
The idea is simple. Try to insert a new Entity object, undo it, fetch all Entity objects in the MOC and print them out. If everything worked correctly, there should be no objects at the end.
However, I get this output:
[Session started at 2010-02-20 13:41:49 -0800.]
2010-02-20 13:41:51.695 Untitledundotes[7373:20b] Insert Value 136
2010-02-20 13:41:51.715 Untitledundotes[7373:20b] FETCHED ENTITY 136
As you can see, the object is present in the MOC after I try to undo its creation.
Any suggestions as to what I am doing wrong?

Your problem is caused by the fact that, unlike OS X, the iPhone managed object context does not contain an undo manager by default. You need to explicitly add one.
Change the generated code in the app delegate for the managedObjectContext property to look like this:
- (NSManagedObjectContext *) managedObjectContext {
if (managedObjectContext != nil) {
return managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
//add the following 3 lines of code
NSUndoManager *undoManager = [[NSUndoManager alloc] init];
[managedObjectContext setUndoManager:undoManager];
[undoManager release];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
return managedObjectContext;
}
After making that change, the 2nd log message is no longer printed.
Hope that helps...
Dave

I tried Dave approach, but did not work for me.
I finally found the solution in Apple's example CoreDataBooks
The trick is to create a new context that shares the coordinator with you App's context. To discard the changes you dont need to do a thing, just discard the new context object. Since you share the coordinator, saving updates your main context.
Here is my adapted version, where I use a static object for the temp context to create a new ChannelMO object.
//Gets a new ChannelMO that is part of the addingManagedContext
+(ChannelMO*) getNewChannelMO{
// Create a new managed object context for the new channel -- set its persistent store coordinator to the same as that from the fetched results controller's context.
NSManagedObjectContext *addingContext = [[NSManagedObjectContext alloc] init];
addingManagedObjectContext = addingContext;
[addingManagedObjectContext setPersistentStoreCoordinator:[[self getContext] persistentStoreCoordinator]];
ChannelMO* aux = (ChannelMO *)[NSEntityDescription insertNewObjectForEntityForName:#"ChannelMO" inManagedObjectContext:addingManagedObjectContext];
return aux;
}
+(void) saveAddingContext{
NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter];
[dnc addObserver:self selector:#selector(addControllerContextDidSave:)
name:NSManagedObjectContextDidSaveNotification object:addingManagedObjectContext];
NSError *error;
if (![addingManagedObjectContext save:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
[dnc removeObserver:self name:NSManagedObjectContextDidSaveNotification object:addingManagedObjectContext];
// Release the adding managed object context.
addingManagedObjectContext = nil;
}
I hope it helps
Gonso

It should work. Did you correctly assign the undo manager to your managedObjectContext? If you have rightly done that, it by default has undo registration enabled, and you should be good to go. There is a good article on core data here. There is a good tutorial on core data and NSUndoManager here. Hope that helps.

Related

Lost data with multiple NSManagedObjectContexts on iOS7

I'm in the progress of updating an existing app for iOS 7 and I've been having some issues with Core Data saving objects. It's a fairly straightforward master-detail style data entry app that uses Core Data for the storage.
When adding a new record I use a second (temporary) managed object context to prevent the record appearing in the list before the record is saved. When a record is added and saved it is visible in the list as expected. However if I exit the app (it doesn't run in the background) and then restart it the record is no longer present. The record is present in the database (visible using the SQLite Manager Firefox plugin anyway), but it just doesn't show in the app.
I've managed to reproduce this using the code that Xcode produces when creating a new project. I've created a new master-detail application and ticked the Use Core Data box to get the example code, then made the following changes:
Add the following to MasterViewController.m
-(void)save:(NSManagedObjectContext*)context
{
if (context != [self.fetchedResultsController managedObjectContext])
{
NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter];
[dnc addObserver:self selector:#selector(addControllerContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:context];
}
NSError *error;
if (![context save:&error])
{
abort();
}
if (context != [self.fetchedResultsController managedObjectContext])
{
NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter];
[dnc removeObserver:self name:NSManagedObjectContextDidSaveNotification object:context];
}
}
- (void)addControllerContextDidSave:(NSNotification*)saveNotification
{
[[self.fetchedResultsController managedObjectContext] mergeChangesFromContextDidSaveNotification:saveNotification];
}
Replace the supplied insertNewObject in insertNewObject with the following to create a new temporary context for adding
- (void)insertNewObject:(id)sender
{
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
context.persistentStoreCoordinator = [[self.fetchedResultsController managedObjectContext] persistentStoreCoordinator];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
// If appropriate, configure the new managed object.
// Normally you should use accessor methods, but using KVC here avoids the need to add a custom class to the template.
[newManagedObject setValue:[NSDate date] forKey:#"timeStamp"];
// Save the context.
[self save:context];
}
I also set the app to not run in the background.
If I run this against iOS 6 it behaves as expected i.e. I tap Add and a new record appears, then exit and restart the app and the record is still present.
However if I run the same code against iOS 7 it doesn't work correctly. Tapping Add causes the new record to appear, but if I exit and them restart the app the record is not shown. As mentioned above it is present in the database however.
Interestingly, I've discovered that it might be in some way related to the change in the journaling mode of the SQLite database. If I add the following options in the call to addPersistentStoreWithType I get the expected behaviour running on iOS 7
NSDictionary *options = #{ NSSQLitePragmasOption : #{#"journal_mode" : #"DELETE"} };
So, to the questions (and thanks for reading this far!)
Has anyone else seen this behaviour (or is anyone able to reproduce it based on the description above)?
Is there something wrong with the way I am using a temporary context that I was just lucky with prior to iOS 7, or does this look like an issue with the Core Data framework on iOS 7?
Cheers
Neil
Edit 1:
In answer to Wain's question about saving the main MOC, I was under the impression that this isn't actually necessary because the data is already saved, the merge just updates the already saved changes from the temporary context in to the main context. That said the test code does contain the following methods and saveContext is called on shutdown, however [managedObjectContext hasChanges] returns false so nothing actually gets done at this point
-(void)applicationWillTerminate:(UIApplication *)application
{
// Saves changes in the application's managed object context before the application terminates.
[self saveContext];
}
-(void)saveContext
{
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil)
{
if ([managedObjectContext hasChanges])
{
if (![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.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
}
}
It seems to be fixed when you save your main context after merging changes:
- (void)addControllerContextDidSave:(NSNotification*)saveNotification
{
[[self.fetchedResultsController managedObjectContext] mergeChangesFromContextDidSaveNotification:saveNotification];
[self save:[self.fetchedResultsController managedObjectContext]];
}
UPDATE: this error was caused by using cache in NSFetchedResultsController. So, the data isn't lost, it's just not displayed by your NSFetchedResultsController. Further investigation is needed to find out why cache isn't updated when its MOC merges changes, but isn't saved.

Bad Access on Core Data deleteObject

I could use some assistance in debugging a EXC_BAD_ACCESS error received on the [context deleteObject:loan]; command. The error is received in the following delegate method:
- (void)didCancelNewLoan:(Loan *)loan {
// save the context
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:loan]; // *** EXC_BAD_ACCESS here ***
// This method is called from a the following method in a second class:
- (IBAction)cancel:(id)sender {
[delegate didCancelNewLoan:self.loan];
}
// The loan ivar is created by the original class
// in the below prepare for Segue method:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"NewLoan"]) {
UINavigationController *navController = (UINavigationController *)[segue destinationViewController];
LoanViewController *loanView = (LoanViewController *)[[navController viewControllers] lastObject];
loanView.managedObjectContext = self.managedObjectContext;
loanView.delegate = self;
loanView.loan = [self createNewLoan];
loanView.newLoan = YES;
}
// Finally, the loan is created in the above
// method's [self createNewLoan] command:
- (NSManagedObject *)createNewLoan {
//create a new instance of the entity managed by the fetched results controller
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[newManagedObject setValue:[NSDate date] forKey:#"timeStamp"];
CFUUIDRef uuid = CFUUIDCreate(NULL);
CFStringRef uuidstring = CFUUIDCreateString(NULL, uuid);
//NSString *identifierValue = (__bridge_transfer NSString *)uuidstring;
[newManagedObject setValue:(__bridge_transfer NSString *)uuidstring forKey:#"identifier"];
CFRelease(uuid);
CFRelease(uuidstring);
NSError *error;
[self.fetchedResultsController performFetch:&error];
NSLog(#"%i items in database", [[self.fetchedResultsController fetchedObjects] count]);
return newManagedObject;
}
Appreciate your looking at the above methods.
Guess #1: you are accessing a deallocated object. To debug: turn on zombies and see what happens.
Update: here's how you turn on zombies in Xcode 5:
Product > Scheme > Edit Scheme, select Diagnostics tab, check "Enable Zombie Objects"
for older Xcode
, edit your build settings, add and enable these arguments in your build scheme:
Guess #2: you have a multithreaded app and you are accessing a managed object context from different threads, which is a no no.
You can add an assert before your delete:
assert( [ NSThread isMainThread ] ) ;
From looking at your code above, there's nothing that stands out as being done incorrectly.
I am wondering whether you are dealing with two different managed object contexts without realising it? You will have to set some breakpoints where you create the Loan object and see if that might be the case.
Also why do you have to get a reference to the context via fetchedResultsController if you already have a declared property for it in self.managedObjectContext ?
The other thing is why do you need to call the fetchedResultsController to performFetch: again when you create a new Loan object? Is your data presented in a table view and have you implemented the NSFetchedResultsController delegate methods?
That call seems unnecessary and it may be causing issues with the cache created by the fetch. See section "Modifying the fetch request" under this link http://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsController_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40008227-CH1-SW24
Finally, try your delete operation directly in the view controller that received the action rather than pass it to the delegate (just to eliminate the possibility that something has been dealloc'd without you knowing).
Here's what I'd do:
- (IBAction)cancel:(id)sender
{
NSError *error;
NSManagedObjectContext *context = [self.loan managedObjectContext];
[context deleteObject:self.loan];
if (![context save:&error])
NSLog (#"Error saving context: %#", error);
}
I got a Bad Access because a deallocated UIViewController was a delegate of a NSFetchedResultsController it had.
The NSFetchedResultsController was deallocated - but when settings a delegate, it observes NSManagedObjectContext for changes, so when NSManagedObjectContext was saved - a bad access would occur when trying to notify the NSFetchedResultsController about the change.
Solution is to clear delegate of NSFetchedResultsController upon deallocation.
- (void)dealloc {
fetchedResultsController.delegate = nil;
}

CoreData DetailTableView BAD_ACCESS Error

Maybe I'm not going about showing a detail for a selected row using CoreData, but I can't figure out why I'm getting a "BAD_ACCESS" error. I've googled around and can't find what I'm looking for.
Basically I use CoreData to populate the data for a Table View. It retrieves all of the title attributes for all of the entities. When the user clicks on a row, I have a Detail View that needs to show the description for that entity. I think I need to make a new NSManagedObjectContext and a new NSEntityDescription for a new NSFetchRequest in my DetailViewController and then use a NSPredicate to say "where title = [user selected title]". I get an error when I select a row. See code:
- (void)viewDidLoad
{
// Do any additional setup after loading the view from its nib.
// Get the objects from Core Data database
Caregiver_Activity_GuideAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:#"Definition"
inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription];
NSPredicate *pred = [NSPredicate predicateWithFormat:#"(title = %#)", self.title];
[request setPredicate:pred];
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
NSLog(#"There was an error!");
// Do whatever error handling is appropriate
}
for (NSManagedObject *oneObject in objects) {
[definitionDescriptionTextView setText:[oneObject valueForKey:#"desc"]];
}
[objects release];
[request release];
[super viewDidLoad];
}
I comment out that code and everything works. But when I try to debug with breakpoints, nothing catches. So I'm more confused.
I know CoreData is probably overkill for what I'm doing but this is a learning app for me.
EDIT: I didn't include that I'm using a sqlite database that is pre-populated with the entities.
You can also download my project on my github page.
Normally, with a Core Data backed Master-Detail interface, you don't fetch for the Detail view.
When you select a row in the Master tableview, you are selecting a particular managed object instance. You then pass that managed object instance to the detail view. There is no need to refetch the object that you selected in the tableview.
A good example of this would be the Contacts app. The Master tableview would be a list of Contact objects (displaying the name.) When you select a row, the Master tableview controller takes the specific Contact object associated with the selected row and then passes it to the Detail view controller which then populates the Detail view using data taking from the properties of the passed Contact object.
So, that entire code block where the error occurs is unnecessary.
However, the immediate error in this code is that you are releasing an object you didn't create. In this line:
NSArray *objects = [context executeFetchRequest:request error:&error];
... you are not creating a NSArray instance with a init, new or create method. Instead, you are merely receiving an autoreleased NSArray instance created and returned by the context NSManagedObjectContext instance. When you release an object you did not create here:
[objects release];
... you cause the crash.
Conversely, you do create a NSFetchRequest here:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
... because you used init so you do have to balance that with:
[request relwase];
BTW, this type of code should not be put in viewDidLoad as the method is only called when the view is read in the first time from the nib file on disk. That is only guaranteed to happen once as the view may remain in memory when the user switches to another view. Instead, put code that needs to run each time the view appears in viewWillAppear.

Why does releasing my CoreData context/entity make my app crash?

I have a simple CoreData app which allows you to add items to a list, displayed in a table view. When the user types in a new item, the following method is called:
- (void)addNewItem:(NSString *)item
{
// Create a new instance of the entity managed by the fetched results controller.
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
Item *newItem = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[newItem setName:item];
// Save the context.
NSError *error = nil;
if (![context save:&error])
{
//error handling code
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[context release];
[entity release];
[newItem release];
The app always allows you to add one item to the list, but then crashes if you try to add a second. If I remove "[newItem release];", the app will allow you to add 4 list items, and then suddenly crash when you try to enter a fifth.
The app will only work properly if all three of those release statements at the end of the method are removed. Can anyone explain why?
The objects are all autoreleased (because you never alloc init anything), so you're not supposed to release them yourself. It's not predictable when your app will crash as far as I can tell, but it will eventually crash.
Just to clarify #BoltClock's answer. It's not about alloc, init only, but there's also new..., copy..., etc.
You should read Memory Management Guide, especially Memory Management Rules.

Core Data Edit Attributes

So im really new to core data, but i went through a tutorial and pretty much understand it, well at least the idea behind most of the things. But I still have 1 question that i cant find anywhere. It seems really simple but here it is. If I were to have two strings inside one entity lets say:
1.name
2.position
If the name is already entered how might i allow a user to enter text into a textField and assign it to their position at a later time? Even if there were 20 names, considering no duplicates?
I was thinking it might be something like this...But it doesnt seem to work.
UserInfo *userInfo = (UserNumber *)[NSEntityDescription insertNewObjectForEntityForName:#"UserInfo" inManagedObjectContext:managedObjectContext];
if ([userName isEqualToString:"#James"]) {
userInfo.Position = nameField.text;
}
On the code above you are casting (UserNumber*) to an object that you are declaring as (UserInfo*)? Which is what and is there any reason why you are doing that?
If I understand your question correctly, you want to create a record with only the username pre-populated and then allow that record to be updated at a later stage.
I will assume your entity is called UserInfo and that there are 2 NSString properties created for it - userName and position. I also assume you have created the class files for UserInfo and imported the header into the relevant view controllers.
Here's how you would do it:
1) Firstly, assuming you have username typed in a UITextField *userNameField, let's create a new record.
UserInfo *userInfo = (UserInfo*)[NSEntityDescription insertNewObjectForEntityForName:#"UserInfo" inManagedObjectContext:self.managedObjectContext];
[userInfo setValue:userNameField.text forKey:#"userName"];
This will create a new instance of UserInfo in your managed object context and set the value of userName to the value on userNameField.text
Then at a later stage a user will get to a point where they can update their records in your app (you may need to think about authentication somewhere here). You will fetch the record that matches your specified username:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSPredicate *userNamePredicate = [NSPredicate predicateWithFormat:#"(userName == %#)", userNameField.text];
[fetchRequest setPredicate:userNamePredicate];
NSEntityDescription *userInfo = [NSEntityDescription entityForName:#"UserInfo" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:userInfo];
NSError *error;
NSArray *fetchRequestArray = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
[fetchRequest release];
If the fetchRequest found match(es) to your userNameField.text paramater, they will be saved in the fetchRequestArray. There should only be a maximum of one object there if you take the necessary steps to make the userName property mandatory AND unique.
Access the object by grabbing the objectAtIndex:0 in the array and change it's position property:
UserInfo *userInfoToBeEdited = [fetchRequestArray objectAtIndex:0];
[userInfoToBeEdit setValue:positionTextField.text forKey:#"position"];
In both cases above, remember to invoke CoreData's save method when you are ready to commit your changes. Before save is invoked your changes are only kept in your managed object context which is basically a scratch pad for your persistent data.
[EDIT TO ADD SAVE METHOD]
As per your comment, I usually have the save method below in my AppDelegate (copy/paste directly from Apple template)
- (void)saveContext
{
error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil)
{
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error])
{
[self seriousErrorAlert];
}
}
}
And then whenever I need to save changes, from any view controller I simply grab a reference to my AppDelegate and fire it off:
AppDelegate *theDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[theDelegate saveContext];