Force save NSManagedObject with NSUndoManager - ios

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.

Related

What am I supposed to do in processContentChanges:?

This is my app's processContentChanges: method, which is triggered by NSPersistentStoreDidImportUbiquitousContentChangesNotification:
- (void)processContentChanges:(NSNotification *)notification {
[self.managedObjectContext performBlock:^{
// Merge incoming data updates in the managed object context
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
// Post notification to trigger UI updates
#warning What do I actually do here?
}];
}
I'm using NSFetchedResultsControllers throughout my app so that the UI updates automatically when changes are received from another device through iCloud. This all seems to work, but the comment saying // Post notification to trigger UI updates was there in the template method already. Am I actually supposed to do something here, or can I safely leave things the way they are?
Well, although I haven't had confirmation of this I don't think there is anything else that needs to be done in this method as long as the following criteria are met:
You implement NSPersistentStoreDidImportUbiquitousContentChangesNotification correctly as per the template so that new content is merged to the managed object context
Your content is generated using NSFetchedResultsController objects
Your viewControllers conforms to the the NSFetchedResultsControllerDelegate protocol, and implements controllerWillChangeContent:, controllerDidChangeContent: and controller:didChangeObject:atIndexPath:forChangeType:newIndexPath
In those methods, update your views accordingly to display new content, remove deleted content, and update changed content.
If you have any objects which utilise CoreData without an NSFetchedResultsController then you might need to update these by manually re-fetching the data when NSPersistentStoreDidImportUbiquitousContentChangesNotification is posted by CoreData.

How to handle NSManagedObjectContext and NSManagedObject creation and editing on multiple threads?

I have an application where I’m using Core Data. It’s my first time with it so I’m using the same Core Data stack that Apple provides in the AppDelegate.m .
The problem I’m facing is described below :
I have a method called firstSaver which performs operations as :
+(void) firstSaver {
// 1) get some values from system
// 2) do some processing on those values ( This takes considerable time)
// 3) create a NSManagedObject instance of entity A ,say mObj ,by filling in the processed values. I create multiple objects. In this step, I use the main managedObjectContext that is provided by the AppDelegate to me.
// 4) pass this NSManagedObject to secondSaver like :
[self secondSaver : mObj];
// 5) save the managedObjectContext.
}
the second method works as :
+(void) secondSaver : (NSManagedObject *)someObj {
// 1) again fetch some values, this too takes considerable time.
// 2) create a NSManagedObject which is instance of entity B, fill the processed values, attach this instance to the someObj instance.
return;
}
Note that A is related to B by a one-to-many relationship, i.e. A contains a NSSet of B.
As seen, the two calls require considerable time to complete and it freezes the UI. I don’t want it to happen hence I created a serial dispatch_queue and called the firstSaver on it using dipatch_async.
The problem is that as the instance of NSManagedObjectContext has been created on the main thread, and if I access it inside dispatch_async, it results in EXEC_BAD_ACCESS.
What could possibly be the correct approach to handle this scenario and use proper managed object context for dealing with multithreading ? Any help will be appreciated.
You should create new child managed object contexts to use, with private queue type and the main context as the parent. All of your logic should be in a performBlockAndWait: and that's where you do your long query and create the new object.
To use the mObj here you need to get its objectID and then use existingObjectWithID:error: to get the appropriate version in the child context. Then you can connect your new object to the existing object but in the correct context.
When you're done, save the child context and then use performBlock on the main context and save it.
// move to background thread
// create child
// set parent
// perform block and wait on new context
// find the existing object for mObj ID
// search, create, associate
// save
// perform block on the main context
// save
Multithreadding with Coredata is a pain in the ass. You should avoid this if possible. If the creation or modification of an MO takes long time, create the data or modify the exiting one in a background thread and then do a performSelectorOnMainthread for all Coredata actions.

Strange parent / child NSManagedObjectContext phenomenon

I have created two context like this:
// create writer MOC
_privateWriterContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateWriterContext setPersistentStoreCoordinator:_persistentStoreCoordinator];
// create main thread MOC
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_managedObjectContext.parentContext = _privateWriterContext;
I have a NSFetchResultedController initiated with _managedObjectContext.
I know it is strange, but I am adding a record to the parent to _privateWriterContext, I am saving it.
What is surprising is that child context and so FRC gets notified about this event. Why? I have not reset-ed child, or anything else. I thought they are independent entities as long as child context will not get saved.
In #pteofil article I found this line:
When a change is made in a context, but not saved, it is visible to all of its’ descendants but not to its’ ancestors.
.. it is pushed to the persistent store (via the persistent store coordinator) and becomes visible to all contexts connected to the store.
This is not supposed to happen. Adding an NSManagedObject ('record' ) to the parentContext, will not make the child aware of this object automatically. Only when you make the childContext execute a fetch, then it will fetch from the parentContext.
To figure out what is going on in your case, here are some suggestions:
figure out when a fetch is executed on the childContext (this is done by the fetchedRestultsController when you set it up. Check if that fetch happens before or after you add the managedObject to the parentContext).
set breakpoints in all four delegate callbacks of the fetchedResultsController to find out for which object it is calling the methods (and see if it is the object you just added to the parentContext).
make absolutely sure you know which context you are sending messages too.
I have a used a similar approach, but different: the childContext is the context is used to parse in new data (on a private queue), and when this parsing is done, the chid calls save:. This will save the changes up to the parent, which is in my case the mainQueueContext. This call to save: will cause the mainQueueContext to receive all the newly parsed objects, and any fetchedResultsController using that mainQueueContext will then call it's delegate methods for the new/changed/updated/delete objects.
You could try inverting your child/parent relationship too and see if it works as described in the docs, just to find out whats going on.
I strongly recommend avoiding parent-child context setups. Our book goes into detail why they often lead to strange behaviour: https://www.objc.io/books/core-data/
Short story: They're not as independent as you might think.
Use multiple context that share a single persistent store coordinator if you need more than a single context.

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:)

save: on a NSManagedObjectContext not working

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];

Resources