save: on a NSManagedObjectContext not working - ios

In a navigation controller, I go from view controller 1 to view controller 2.
View Controller 1 is a table view controller tied to a FetchedResultsController. It fetches data from a specific context and displays it. I then segue to view controller 2 if a row is tapped. While segueing, I set a particular NSManagedObject property of view controller 2 using the data I have in view controller 1.
Now, in view controller 2 I am able to show data using the NSManagedObject property and then also make changes to it and perform a save:. When I go back to view controller 1 , the change is reflected. However, if I re-launch the app it is not being reflected any more in any of the view controllers.
This is how I am doing the save.
- (void)hiddenAttributeSwitchSlid:(UISwitch *)sender
{
[self.workoutType.managedObjectContext performBlock:^{
self.workoutType.workoutHiddenByDefault = [NSNumber numberWithBool:sender.isOn];
NSError *error = nil;
if (![self.workoutType.managedObjectContext save:&error]) {
NSLog(#"There was an error in saving to context - %#", [error localizedDescription]);
}
else {
NSLog(#"No error");
}
}];
}
workoutType is a NSManagedObject which is set in the prepareForSegue: before segueing to this view controller.
Even if I do not use the performBlock:, it doesn't work.
PS - I know questions of this kind have been asked before. I browsed through them, but nothing seems to be working.

Do you have a default value set for that attribute in your model?
There is an identified bug in Core Data where in some situations (I have not narrowed it all the way down) where an attribute with a default value gets reset several times during the backgrounding of an application.
The way to test for this is to listen for the value being changed via KVO and log the changes then duplicate your test. If you see several changes then you know you are hitting that bug.
The only known solution I have seen that is reliable is to remove the default value. If a default value is required then I would add it to the NSManagedObject subclass in the -awakeFromInsert method and then update the validation methods to check for it. Sucks I know.
Update #2
How many contexts do you have?
Are you using parent child contexts?
If so, are you saving the top most parent?
Update #3
Ok, a UIManagedDocument has two NSManagedObjectContext instances inside of it. Is there a reason you are using a UIManagedDocument? Do you have more than one document in your application? If you don't then I strongly suggest you switch back to a traditional Core Data stack. The UIManagedDocument is not really meant to be in a single stack application.
As for the saving issue directly, UIManagedDocument tries to save in the background when you exit the application. If can take a bit of time and, personally, is not very reliable. You can request a save on exit which will help to insure the save is speedy, but even then it can be unreliable.
How are you exiting the application?
Are you killing it in Xcode?
Are you backgrounding it and then resuming it?
Are you backgrounding it and then killing it from the iOS device?
You can listen for the UIManagedDocument to save and then print a log statement so you can see when the saves actually happen to disk. That might be useful to help narrow down exactly when it is and is not saving.

I think you shouldn't use saving on context, the document will auto save.
There might be a data consistency error when the document is being saved. I propose to you not to use saving of the context by [self.workoutType.managedObjectContext save:&error] and use the following class inherited from UIManagedDocument which adds logging on auto-save:
LoggingManagedDocument.h:
#interface LoggingManagedDocument : UIManagedDocument
#end
LoggingManagedDocument.m:
#import "LoggingManagedDocument.h"
#implementation LoggingManagedDocument
- (id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
NSLog(#"Auto-Saving Document");
return [super contentsForType:typeName error:outError];
}
- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted
{
NSLog(#"error: %# userInfo: %#", error, error.userInfo);
}
#end
This class should help you identify the problem, why your data is not saved. Just let the application run and wait 15-30 seconds after you make the change of the attribute and auto-save should occur.
The logging is based on: http://blog.stevex.net/2011/12/uimanageddocument-autosave-troubleshooting/

From the UIManageDocument documentation:
The UIManagedDocument architecture leads to several considerations:
You should typically use the standard UIDocument methods to save the document.
If you save the child context directly, you only commit changes to the parent context and not to the document store. If you save the parent context directly, you sidestep other important operations that the document performs.
The easiest option is to use a saveless model, which means using the NSUndoManager of the document. I usually do [self.workoutType.managedObjectContext.undoManager beginUndoGrouping]; before adding the changes and [self.workoutType.managedObjectContext.undoManager endUndoGrouping]; after editing.

You need to save the document:
[document saveToURL:document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];

Related

Force save NSManagedObject with NSUndoManager

I have an application listening to server events through a websocket. When the server sends a specific event, I create a Notification which is a subclass of NSManagedObject. I later display it in my main view controller.
In a view controller (let's call it ObjectViewController), I have this code :
- (void)viewDidLoad {
[super viewDidLoad];
[((AppDelegate *)UIApplication.sharedApplication.delegate).managedObjectContext.undoManager beginUndoGrouping];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
AppDelegate * delegate = ((AppDelegate *)UIApplication.sharedApplication.delegate);
if (something) {
[delegate saveContext];
} else {
[delegate.managedObjectContext undo];
}
}
It allows me to undo all operation done to several NSManagedObjects of different types when I click the cancel button.
Now the problem is, when I am in this view and I receive a notification, the Notification object is removed from CoreData if I cancel the object changes.
Is there a way to force CoreData to save ONE notification while the other NSManagedObjects remain in the undo group ?
When you save a context, it saves everything in the context.
IMO, a better approach would be to use a separate NSManagedObjectContext as a "scratchpad".
Basically, your view controller will create its own context, either as a child of the main context, or connected directly to the main context's persistent store coordinator (if the latter, you need to merge saved changed).
This use case, however, is probably best served by creating a child context.
This way, your "editing context" is separate from the main context. When the view controller goes away, you can save the context, or simply do nothing and let it dealloc.
Thus, you can still have changes going on in your "main context" and anything done in the "editing context" only happens if you choose to save the context.
You can then just not even use the undo manager, as the temporary context is doing that work.
EDIT
After looking at the apple doc, to create a new context as a child of
the main context, I just have to set its parentContext attribute ? I
don't know how I lived without knowing this... so useful ! – Sunder
To create it, yes, that's all you have to do. There are some drawbacks to using it, but there are usually corner cases. As long as you are not creating new objects in the child context and passing their object-ids to another MOC, you should be fine.
Simply make your changes, and if you want to share them with the parent, just save to the parent. Note, however, that saving from a child to a parent just "copies" the object changes into the parent. The parent context must still save its context for the changes to make it to the store.

Core Data: A clever way to implement Add/Edit functionality

I've created a couple of Apps that use Core Data and a lot of experiments, but I've never found the "perfect" way to implement a simple Add/Edit viewController.
I just want to implement a single controller able to manage both the edit and add functionalities, I don't want to create two different Controllers.
At the moment I'm working whit this approach (let's take the classic Person NSManagedObject as example)
1) In the addEditViewController I add a currentPerson property
var currentPerson:Person?
2) When I present the controller in Add-Mode this property is nil
3) When I present the controller in Edit-Mode this property is a reference to the Person to edit
4) When I need to save user operations I just check if the currentPerson is set and I understand if I need to create a new object in the context or just save the one I need to edit.
Ok, this approach works but I want to follow another approach that seems to be more secure for the edit action. Check this terrible error!
Let's say that the person has Address property that needs a different viewController to be edited.
1) Following my previous logic I can pass the currentPerson property to the addressViewController that I'm going to present:
addressVC.currentPerson = currentPerson
presentAddressVC()
2) Now when the user has completed the edit operation and he/she taps on "save"
the addressVC calls the saveContext function.
Where is the problem? well... if the user starts editing the currentPerson in the addEditViewController an then just goes back to a previous controller, the currentPerson still stores the edit of the user and as soon as the context will be saved in any other controller the not-really-wanted data get stored and becomes persistent.
Probably I can perform a rollback in case the user taps the back button on the addEditViewController, but I really don't like this behaviour it seems so poor.
I think to work with multiple contexts or inserting NSManagedObjects in a nil context and just move them to the main context only at the end of the operations but I'm not sure about this choice too.
I know it's a kind of a complex and long (an tedious) question, but I hope you can give me some lights on this issue.
How to you treat this kind of situation? what do you think about my approach and about my proposed approaches?
It seems to me that your problem is maintaining a connection to a single NSManagedObjectContext when instead what you really want is to establish a tree. The construction of a context is fairly cheap so you should be creating a context per ViewController.
So when you show the addEdit controller you can simply create it with a new context:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;
context.parentContext = //parentContext
context.undoManager = nil;
Think of these new contexts as scratch pads for editing your managed objects. The only thing to bear in mind is that when you call save, it saves to the parent context and not all the way to the store. For that you will need a recursive call all the way to the parent for saves. Heres a basic recursive save:
- (void)saveChangesToDiskForManagedObjectContext:(NSManagedObjectContext *)context {
if (context && [context hasChanges]) {
[context save:nil];
NSManagedObjectContext *parentContext = context.parentContext;
[parentContext performBlock:^{
[self saveChangesToDiskForManagedObjectContext:parentContext];
}];
}
}
Its not really great practice to retain managed objects in an app where they could be deleted on other screens. So what you should do is perhaps fetch these objects in your view controllers view will appear method. Either that or call refreshObject(mergeChanges:) to synchronize your managed object with the changes made by another screen.
I really don't like the idea of calling save when the user navigates back, there should be a save button, when i press back I'm trying to close the screen, i would expect to select "done" if i want my changes saved.
Don't forget you can also use an NSUndoManager to track all your changes, thats why the context has an undoManager:)

Consistency between the same database accessed in the appdelegate and a view controller

I've been working with Core Data and UIManagedDocument lately, and have some questions.
I've a UITableViewController in which I open a UIManagedDocument. Here I download info to the tableViewCotroller and save the info to the database and update the tableview. Now when I enter the main screen via the home button and then open the app again, I want to do work with the data from the database in the appdelegate . So in the appropriate method in appdelegate, I open the database, fetch the entities I want and change the properties in the objects I want. Now, this works, I manage to open the database and get the objects I want From the database.
My problem occours when I'm done changing data from the database in the appdelegate. Because after the appdelegate method is completed, I try to refetch data in the tableview from the same database, but at this point of time the database hasn't registered the changes made in the method called in the appdelegate. I've tried calling the saveToURL for overwriting method on the UIManagedDocument right after the changes are made inside the appdelegate and ive also tried the save: method on the managedobjectcontext.
What's the right way of saving the changes so that database gets updated?
Ps: I'm writing this on an iPad, so I havnt managed to upload code, I'll upload it later.
Thanks
That is the right way - make changes in a NSManagedObjectContext then save. Remember that saveToURL is asynchronous, so what is most likely the problem is that your save has not completed by the time you perform the fetch in the table view. That's where the completion block comes in. So maybe you could have the completion block set a property in your table view which causes your fetch to happen. Something like
// appdelegate
[self.mainDatabase saveToURL:self.mainDatabase.fileURL
forSaveOperation:UIDocumentSaveForOverwriting
completionHandler:^(BOOL success) {
if (success) {
myTableView.context = self.mainDatabase.managedObjectContext;
}
}];
// TableView
- (void)setContext:(NSManagedObjectContext *)newContext {
_context = newContext;
NSArray *fetchResults = [_context executeFetchRequest:someFetchRequest error:&error];
// update model etc.
}

Not getting current data in UITableView after XML Parse (using NSFetchedResultsController)

Struggling a bit here...
My view controller adheres to following protocols
In my init method I will check a remote server to get an updated XML file... parse the XML file, and write the contents to Core Data.
My tableview's content is managed with NSFetchedResultsController that displays this Core Data.
My Problem:
NSFetchedResultsController seems to be getting the data before the Core Data update from the remote file takes place. I've verified the database is being updated properly and if I run a second time the TableView will show the correct data.
Maybe I'm just not doing the reloadData in the proper place? I have implemented
-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[myTableView reloadData];
}
Also, after the parser completes and the new data has been written to core data I'm trying this:
-(void)parserDidEndDocument:(NSXMLParser *)parser {
[myTableView reloadData];
}
Anyone have any ideas? Let me know what extra code might be useful to post. Thanks!
you may want to check the following (from Apple's docs)
A controller thus effectively has three modes of operation, determined by whether it has a delegate and whether the cache file name is set.
No tracking: the delegate is set to nil.
The controller simply provides access to the data as it was when the fetch was executed.
Memory-only tracking: the delegate is non-nil and the file cache name is set to nil.
The controller monitors objects in its result set and updates section and ordering information in response to relevant changes.
Full persistent tracking: the delegate and the file cache name are non-nil.
The controller monitors objects in its result set and updates section and ordering information in response to relevant changes. The controller maintains a persistent cache of the results of its computation.
it sounds like you want full persistent tracking. So you probably want to make sure you have the delegate set (which you probably already have done) and set the cache to non nil
You may also want to make sure you are saving your managedObjectContext after you are done parsing. After saving, make sure to perform the fetch again.
NSError *error;
BOOL success = [controller performFetch:&error];
if (!success)
NSLog(#"Core Data Fetch Error: %#"error);
It could be that the app is saving the context when it is exiting and that is why you are seeing the data when you relaunch.
Good luck

iOS - Core data - NSManagedObjectContext - not sure if it is saved

Overview
I have an iOS project in which I am using Core data
I am inserting an object, then I want to save it.
I am not sure if save works.
Save seems to be working when app goes into background
When using Simulator, If I click on Stop button on Xcode, save doesn't seem to be working.
Question
Is the save actually happening ?
Am I facing a problem because I created a view based app (the core data checkbox was not available) ?
Steps Followed
I am using the simulator to test it.
Insert an object (code is in the next section)
Save the inserted object (code is in the next section)
I press the Stop button on Xcode to stop running the app
Output noticed
setBeforeSave.count = 1
setAfterSave.count = 0
Before saving, The NSManagedObjectContext method insertedObjects returns 1 object
Before saving, The NSManagedObjectContext method insertedObjects returns 0 objects
When Xcode Stop button is pressed, and when the app is relaunched, the previous data is not available (is it because I clicked on stop on xcode)
managedObjectContext is NOT nil
The NSManagedObjectContext method save: returns YES.
Code to Insert Object
Test *test = [NSEntityDescription insertNewObjectForEntityForName:#"Test" inManagedObjectContext:self.database.managedObjectContext];
Code to Save:
//database is a property of the type UIManagedDocument
NSSet *setBeforeSave = [self.database.managedObjectContext insertedObjects];
NSLog(#"setBeforeSave.count = %i", setBeforeSave.count);
NSError *error = nil;
if(![self.database.managedObjectContext save:&error])
NSLog(#"error = %#", error);
NSSet *setAfterSave = [self.database.managedObjectContext insertedObjects];
NSLog(#"setAfterSave.count = %i", setAfterSave.count);
According to the UIManagedDocument documentation, you should not call save on either of the internal managed contexts. Instead, if you want data saved, you should do one of two things.
Use the undoManager, as it will mark the context dirty, and ready to be saved.
Call [document updateChangeCount:UIDocumentChangeDone];
Thus, in your case, you should replace that save call with:
[self.database updateChangeCount:UIDocumentChangeDone];
And your data will get saved.
EDIT
To provide additional detail. A UIManagedDocument has two MOCs., in a parent/child relationship. The child is the one you get when calling document.managedObjectContext. Now, when a NSManagedObjectContext has a parent, the normal way to propagate changes to the parent is to call save:. However, the UIManagedDocuememt does other stuff, and its documentation specifically says NOT to call save on either the parent or child context.
Well, how does stuff get saved, then? Well, you tell the UIManagedDocument that it is "dirty" and needs to be saved. The two ways you can do that are by either using the undoManager, or calling updateChangeCount:.
When doing either of those, the internals of UIManagedDocument will make sure that the parent context is notified of the change. At some point in the future, the parent will effect the change to the actual backing store (i.e., file(s) on disk).
Furthermore, when a context is "saved" it may or may not keep references to the objects that were changed. You can set a property which tells it to retain objects that have been saved, or to release them.
Hopefully, that addresses your problems.
to summarize, though, see the original answer.
BTW, you can actually see a log of what the SQL store is doing underneath by adding "-com.apple.CoreData.SQLDebug 1" to your command line arguments. You do that in the "Edit Scheme" dialog.

Resources