iOS: Core data performBlock makes UI irresponsive - ios

I just wanted to be clear about core data private context. I am trying to insert 20k record using private context (NSPrivateQueueConcurrencyType). But as soon as 'Insert 20k records' button is tapped UI thread hangs.
- (IBAction)insertRecords:(id)sender {
[[CoreDataStore privateContext] performBlock:^{
NSLog(#"Starting to insert 20k records...");
for (int i = 0 ; i < 20000; i++)
{
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:#"Event" inManagedObjectContext:[CoreDataStore privateContext]];
[newManagedObject setValue:[NSDate date] forKey:#"time"];
}
NSError * error = nil;
NSLog(#"Inserted 20k records to managed object context");
[[CoreDataStore privateContext] save:&error];
NSLog(#"Save context command fired");
}];
NSLog(#"returning from insert method");
}
changes from private context are being merged to main context here
- (void)contextDidSavePrivateQueueContext:(NSNotification *)notification
{
#synchronized(self) {
[self.mainContext performBlock:^{
NSLog(#"merging changes to main context.....");
[self.mainContext mergeChangesFromContextDidSaveNotification:notification];
NSLog(#"merged changes to main context");
}];
}
}
Below is log window output when I hit insert button:
2014-08-04 14:54:12.431 CoreDataDrillDown[11323:90b] returning from insert method
2014-08-04 14:54:12.431 CoreDataDrillDown[11323:1403] Starting to insert 20k records...
2014-08-04 14:54:12.506 CoreDataDrillDown[11323:1403] Inserted 20k records to managed object context
2014-08-04 14:54:12.785 CoreDataDrillDown[11323:90b] merging changes to main context.....
2014-08-04 14:54:12.786 CoreDataDrillDown[11323:1403] Save context command fired
2014-08-04 14:54:27.019 CoreDataDrillDown[11323:90b] merged changes to main context
For more than 15 sec UI was irresponsive. I want to know the reason why?
With below approach suggested by https://stackoverflow.com/users/817182/thom-ek and https://stackoverflow.com/users/2128900/micha%c5%82-ciuba, insertion is quick and does not freeze UI but changes are not getting saved to disk, not sure why.
//writerContext has persistent store coordinator, so it should write data to disk but it
//is not writing any changes to disk.???????
-(NSManagedObjectContext *)writerContext
{
if (!_writerContext)
{
_writerContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_writerContext.persistentStoreCoordinator = [(AppDelegate *)[[UIApplication sharedApplication] delegate] persistentStoreCoordinator];
}
return _writerContext;
}
//mainContext is being used with NSFetchedResultController
-(NSManagedObjectContext *)mainContext
{
if (!_mainContext)
{
_mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainContext.parentContext = [self writerContext];
}
return _mainContext;
}
//performBlock is being called on privateContext
-(NSManagedObjectContext *)privateContext
{
if (!_privateContext)
{
_privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_privateContext.parentContext = [self mainContext];
}
return _privateContext;
}

Main problem is that mainContext is still tied to mainQueue, so it will wait for this huge merge.
There are good articles about asynchronous saves: on Cocoanetics and ObjC.io.
Other solution is just to reset mainContext and reload all tableViews (you don’t show all of 20k records to user at once).

Related

Strange issue with privateQueue managedObjectContext

SETUP (You can read this later and skip to the scenario section first)
It's an old app, with manually setup CoreData stack like this:
+ (NSManagedObjectContext *)masterManagedObjectContext
{
if (_masterManagedObjectContext) {
return _masterManagedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self createPersistentStoreCoordinator];
if (coordinator != nil) {
_masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_masterManagedObjectContext.retainsRegisteredObjects = YES;
_masterManagedObjectContext.mergePolicy = NSOverwriteMergePolicy;
_masterManagedObjectContext.persistentStoreCoordinator = coordinator;
}
return _masterManagedObjectContext;
}
+ (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext) {
return _managedObjectContext;
}
NSManagedObjectContext *masterContext = [self masterManagedObjectContext];
if (masterContext) {
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_managedObjectContext.retainsRegisteredObjects = YES;
_managedObjectContext.mergePolicy = NSOverwriteMergePolicy;
_managedObjectContext.parentContext = masterContext;
}
return _managedObjectContext;
}
+ (NSManagedObjectContext *)newManagedObjectContext
{
__block NSManagedObjectContext *newContext = nil;
NSManagedObjectContext *parentContext = [self managedObjectContext];
if (parentContext) {
newContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
newContext.parentContext = parentContext;
}
return newContext;
}
And then save context recursively:
+ (void)saveContext:(NSManagedObjectContext *)context
{
[context performBlockAndWait:^{
if (context.hasChanges && context.persistentStoreCoordinator.persistentStores.count) {
NSError *error = nil;
if ([context save:&error]) {
NSLog(#"saved context: %#", context);
// Recursive save parent context.
if (context.parentContext) [self saveContext:context.parentContext];
}
else {
// do some real error handling
NSLog(#"Could not save master context due to %#", error);
}
}
}];
}
SCENARIO
The app load lots of data from a server, then perform update inside newContext first, then merge into mainContext -> masterContext -> persistentStore.
Because lots of data, the sync process has been divided into about 10 async threads => we have 10 newContext at a time.
Now, the data is complicated, with things like parents <-> children (same class). 1 parent can have many children, and a child can have a mother, father, god father, step mother..., so it's n-n relationship. First, we fetch parent, then perform fetch child and then set the child to parent, and so on.
The server is kinda stupid, it can't send disabled objects. However the customer would like to control the display of app's objects from the back end, so I have 2 properties to do that:
hasUpdated: At the beginning of loading process, perform a batch update, set all object's hasUpdated to NO. When got data from the server, update this property to YES.
isActive: When all loading was done, perform batch update this property to NO if hasUpdate == NO. Then, I have a filter that won't show object with isActive == NO
ISSUE
Customers complain why some objects being missing even if they're enable in the backend. I've struggle and debugging for so long after got to this strange issue:
newContext.updatedObjects : { obj1.ID = 100, hasUpdated == YES }
"saved newContext"
mainContext.updatedObjects: {obj1.ID = 100, hasUpdated == NO }
// I'll stop here. Obviously, master got updated = NO and finally isActive will set to no, which cause missing objects.
If it happened every time, then probably easier to fix (¿maybe?). However, it occurs like this:
First time running (by first time, I mean app start from where appDidFinishLaunch... got called): all correct
2nd time: missing (153 objects)
3rd time: all correct
4th time: missing (153 objects) (again? exactly those with multiple parents, I believe so!)
5th time: correct again
... so on.
Also, it looks like this happened for objects which have the same context (same newContext). Unbelievable.
QUESTIONS
Why is this happening? How do I fix this? If those objects don't have children, my life would be easier!!!!
BONUS
In case you'd like to know how the batch update is, it's below. Note:
Download requests are in async queue: _shareInstance.apiQueue = dispatch_queue_create("product_request_queue", DISPATCH_QUEUE_CONCURRENT);
Parse response and update properties are syncronous in a queue: _shareInstance.saveQueue = dispatch_queue_create("product_save_queue", DISPATCH_QUEUE_SERIAL);
Whenever parse complete, I perform save newContext and call for updateProductActiveStatus: in the same serial queue. If all requests are finished, then perform batch update status. Since request are done in concurent queue, it's always finished earlier than save (serial) queue, so it's pretty much fool proof process.
Code:
// Load Manager
- (void)resetProductUpdatedStatus
{
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.propertiesToUpdate = #{ #"hasUpdated" : #(NO) };
request.resultType = NSUpdatedObjectsCountResultType;
NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
NSLog(#"Batch update hasUpdated: %#", result.result);
[self.masterContext performBlockAndWait:^{
[self.masterContext refreshAllObjects];
[[CoreDataUtil managedObjectContext] performBlockAndWait:^{
[[CoreDataUtil managedObjectContext] refreshAllObjects];
}];
}];
}
- (void)updateProductActiveStatus:(SyncComplete)callback
{
if (self.apiRequestList.count) return;
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.predicate = [NSPredicate predicateWithFormat:#"hasUpdated = NO AND isActive = YES"];
request.propertiesToUpdate = #{ #"isActive" : #(NO) };
request.resultType = NSUpdatedObjectsCountResultType;
NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
NSLog(#"Batch update isActive: %#", result.result);
[self.masterContext performBlockAndWait:^{
[self.masterContext refreshAllObjects];
NSManagedObjectContext *maincontext = [CoreDataUtil managedObjectContext];
NSLog(#"Refreshed master");
[maincontext performBlockAndWait:^{
[maincontext refreshAllObjects];
NSLog(#"Refreshed main");
// Callback
if (callback) dispatch_async(dispatch_get_main_queue(), ^{ callback(YES, nil); });
}];
}];
}
mergePolicy is evil. The only correct mergePolicy is NSErrorMergePolicy any other policy is asking core-data to silently fail and not update when you expect it too.
I suspect that your problem is that you are writing simultaneously to core-data with the background contexts. (I know that you say you have a serial queue - but if you call performBlock inside the queue then each block is executed simultaneously). When there is a conflict stuff gets overwritten. You should only write to core-data in one synchronous way.
I wrote an answer on how to accomplish this with a NSPersistentContainer:
NSPersistentContainer concurrency for saving to core data and I would suggest that you migrate your code to it. It really should not be that hard.
If you want to keep the code as close to what is currently is as possible that also is not that hard.
Make a serial operation queue:
_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;
And do all writing using this queue:
- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
void (^blockCopy)(NSManagedObjectContext*) = [block copy];
[self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
NSManagedObjectContext* context = [CoreDataUtil newManagedObjectContext];
[context performBlockAndWait:^{
blockCopy(context);
[CoreDataUtil saveContext:context];
}];
}]];
}
Also it could be that the objects ARE updated, but you aren't seeing it because you are relying on a fetchedResultsController to be updated. And fetchedResultsController don't update from batch update requests.

Updating UI while importing large data sets in Core Data

In my app I have a function that will download a large data set from a server into Core Data. It looks something like this:
for (PFObject *historyObject in historyArray) {
History *history = (History *)[NSEntityDescription insertNewObjectForEntityForName:#"History" inManagedObjectContext:managedObjectContext];
history.date = historyObject.date;
// ...
}
Because this can take a few minutes I want to update UI in the process and show the user the progress that has been made.
Now because inserting this data into Core Data is blocking the main thread I am not sure if there is another proper way to update the UI.
I have tried several methods of moving the insertion into a different thread but have always had problems with Core Date responding with errors.
I hope somebody out there has a good idea and would like to share it.
Any help is much appreciated!
I have currently solved this by initializing the ManagedObjectContext with concurrency type NSPrivateQueueConcurrencyType and setting the parent context to the main ManagedObjectContext.
For anybody having the same problem:
- (void)doSomething
{
_backgroundMOC = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundMOC setParentContext:[kDelegate managedObjectContext]];
for (int i = 0; i < [cars count]; i++)
{
[_backgroundMOC performBlockAndWait:^{
Drive *drive = (Drive *)[NSEntityDescription insertNewObjectForEntityForName:#"Drive" inManagedObjectContext:_backgroundMOC.parentContext];
....do more stuff...
}];
}
[self performSelectorOnMainThread:#selector(showProgress:) withObject:[NSNumber numberWithFloat:((float)i/(float)[cars count])] waitUntilDone:NO];
}
For this to work you will have to change the managedObjectContext in the AppDelegate:
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
{
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}

CoreData child contexts, NSFetchedResultsController and main thread

Following this excellent post by Olivier Drobnik, I've implemented the three-layer CoreData stack proposed by CoreData guru Marcus S. Zarra:
The only difference from this diagram and my code is that I only use one Temporary Background MOC, in order to avoid duplicates when inserting objects in several temp MOCs. Here's my context initialisation code:
#pragma mark - NSManagedObjectContexts
+ (NSManagedObjectContext *)privateManagedObjectContext
{
if (!_privateManagedObjectContext) {
// Setup MOC attached to PSC
_privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateManagedObjectContext setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
// Add notification to perform save when the child is updated
_privateContextSaveObserver =
[[NSNotificationCenter defaultCenter]
addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
NSManagedObjectContext *savedContext = [note object];
if (savedContext.parentContext == _privateManagedObjectContext) {
[_privateManagedObjectContext performBlock:^{
NSLog(#"AMBCoreData -> saving privateMOC");
NSError *error;
if (![_privateManagedObjectContext save:&error]) {
NSLog(#"AMBCoreData -> error saving _privateMOC: %# %#", [error localizedDescription], [error userInfo]);
}
}];
}
}];
}
return _privateManagedObjectContext;
}
+ (NSManagedObjectContext *)mainUIManagedObjectContext
{
if (!_mainUIManagedObjectContext) {
// Setup MOC attached to parent privateMOC in main queue
_mainUIManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainUIManagedObjectContext setParentContext:[self privateManagedObjectContext]];
// Add notification to perform save when the child is updated
_mainUIContextSaveObserver =
[[NSNotificationCenter defaultCenter]
addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
NSManagedObjectContext *savedContext = [note object];
if (savedContext.parentContext == _mainUIManagedObjectContext) {
NSLog(#"AMBCoreData -> saving mainUIMOC");
[_mainUIManagedObjectContext performBlock:^{
NSError *error;
if (![_mainUIManagedObjectContext save:&error]) {
NSLog(#"AMBCoreData -> error saving mainUIMOC: %# %#", [error localizedDescription], [error userInfo]);
}
}];
}
}];
}
return _mainUIManagedObjectContext;
}
+ (NSManagedObjectContext *)importManagedObjectContext
{
if (!_importManagedObjectContext) {
// Setup MOC attached to parent mainUIMOC in private queue
_importManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_importManagedObjectContext setParentContext:[self mainUIManagedObjectContext]];
}
return _importManagedObjectContext;
}
This code is pretty straightforward. I'm replicating the above diagram using only the mainUIManagedObjectContext in the NSMainQueueConcurrencyType. Every time the child context, importManagedObjectContext gets saved, a notification is fired and all the parent contexts performs a save in it's current thread.
I've implemented a test view controller with a UITableView and a NSFetchedResultsController attached. This is the code in the viewDidLoad of my test view controller:
- (void)viewDidLoad
{
[super viewDidLoad];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Task"];
[request setSortDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"insertDate" ascending:NO]]];
self.fetchRequest = request;
NSFetchedResultsController *frc =
[[NSFetchedResultsController alloc]
initWithFetchRequest:self.fetchRequest
managedObjectContext:[AMBCoreData mainUIManagedObjectContext]
sectionNameKeyPath:nil
cacheName:nil];
frc.delegate = self;
[self setFetchedResultsController:frc];
[self.fetchedResultsController performFetch:nil];
}
Here I attach the mainUIManagedObjectContext to the NSFetchedResultsController. Later, in my viewDidAppear, I run a loop to insert a few Task entities:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[[AMBCoreData importManagedObjectContext] performBlock:^{
for (int i = 0; i < 5000; i++) {
Task *task = [NSEntityDescription insertNewObjectForEntityForName:#"Task" inManagedObjectContext:[AMBCoreData importManagedObjectContext]];
task.title = [NSString stringWithFormat:#"Task %d", i];
task.insertDate = [NSDate new];
[[AMBCoreData importManagedObjectContext] save:nil];
}];
}
The thing is, I'm inserting 5000 objects and the UI is freezing when the data is populated into the table view. Florian Kugler ran a test with this architecture inserting 15.000 objects and with instruments he got this main thread usage (blue is for main thread, gray for any other threads):
But here's my main thread CPU usage with 5000 objects, profiled using an iPhone 5:
As you can see, my main thread usage is far greater than Florian's and also my UI freezes for a few seconds. My question is, am I doing something wrong? Is this the expected behaviour when using this three-layer MOC architecture with a NSFetchedResultsController and a UITableView? I know that inserting 5000 objects is not the usual behaviour of most apps, so when I've tried with 50 or 100 objects the freeze was inexistent or unnoticeable, but the main thread usage was high (although I admit that in this case it can be due another reasons like waking up the app).
Yes, it is expected, because Main MOC is involved in the saves of its children. It is convenient and kind of okay when children of the UI context don’t do big saves, but often becomes a performance problem if those saves are bigger. You can’t be sure that the UI thread does only minimum job when using this pattern.
For the big saves I would recommend creating a context that is configured directly with the persistent store coordinator. After big save happens, you just refetch and optionally refresh data in the UI context. For more details see my answer here.

Core data save in Background with one-to-many relationship

in a core data app with a one-to-many relationship (one "test", many "measures"), I used to have this code :
In AppDelegate.m :
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
In TableViewController.m :
- (NSManagedObjectContext *)managedObjectContext
{
NSManagedObjectContext *context = nil;
id contextDelegate = [[UIApplication sharedApplication] delegate];
if ([contextDelegate performSelector:#selector(managedObjectContext)])
context = [contextDelegate managedObjectContext];
return context;
}
- (void)saveEntryButton:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
if (self.test)
{
// Update existing test
self.test.number = self.numberTextField.text;
}
else // Create new test
{
self.test = [NSEntityDescription insertNewObjectForEntityForName:#"Test" inManagedObjectContext:context];
self.test.number = self.numberTextField.text;
}
if (isSaving)
{
NSManagedObjectContext *context = [test managedObjectContext];
self.measure = [NSEntityDescription insertNewObjectForEntityForName:#"Measure" inManagedObjectContext:context];
[test addWithMeasureObject:measure];
NSData *newDataArray = [NSKeyedArchiver archivedDataWithRootObject:plotDataArray];
self.measure.dataArray = newDataArray;
}
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error])
{
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
}
It works great, but of course, the [NSKeyedArchiver archivedDataWithRootObject:plotDataArray]; can take a few seconds and block the UI so I would like to do it in background.
I spent a few hours to read everything about the concurrency in core data (and I am quite new at it), but I didn't find anything regarding my problem : how to deal with a one-to-many relationship background save ?
What I've tried so far :
In AppDelegate.m
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:_persistentStoreCoordinator];
//_managedObjectContext = [[NSManagedObjectContext alloc] init];
//[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
In TableViewController.m
- (void)saveEntryButton:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
if (self.test)
{
// Update existing test
self.test.number = self.numberTextField.text;
}
else // Create new test
{
self.test = [NSEntityDescription insertNewObjectForEntityForName:#"Test" inManagedObjectContext:context];
self.test.number = self.numberTextField.text;
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error])
{
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
}
if (isSaving)
{
NSManagedObjectContext *context = [test managedObjectContext];
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.parentContext = context;
[temporaryContext performBlock:^{
self.measure = [NSEntityDescription insertNewObjectForEntityForName:#"Measure" inManagedObjectContext:temporaryContext];
[test addWithMeasureObject:measure];
NSData *newDataArray = [NSKeyedArchiver archivedDataWithRootObject:plotDataArray];
self.measure.dataArray = newDataArray;
// push to parent
NSError *error;
if (![temporaryContext save:&error])
{
// handle error
NSLog(#"error");
}
// save parent to disk asynchronously
[context performBlock:^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
NSError *error;
if (![context save:&error])
{
// handle error
NSLog(#"error");
}
}];
}];
}
}
Of course, I receive a SIGABRT error as "test" and "measure" are not in the same context...I've tried a LOT of different things, but I'm really lost.
Thanks in advance for any help.
I see two questions here: background asynchronous saving and what to do with objects in different contexts.
First about saving. Are you sure that it is saving itself that blocks your UI thread and not call to archivedDataWithRootObject? If saving itself is relatively fast, you can consider calling only archivedDataWithRootObject on a background queue, and then communicating the results back to the main queue where you’ll do the save on your UI context.
If it is still the save that takes too long, you can use the approach for background asynchronous saving recommended by Apple. You need two contexts. One context – let’s call it background – is of private queue concurrency type. It is also configured with persistent store coordinator. Another context – let’s call it UI – is of main queue concurrency type. It is configured with background context as a parent.
When working with your user interface, you’re using the UI context. So all managed objects are inserted, modified, and deleted in this context. Then when you need to save you do:
NSError *error;
BOOL saved = [UIContext save:&error];
if (!saved) {
NSLog(#“Error saving UI context: %#“, error);
} else {
NSManagedObjectContext *parent = UIContext.parentContext;
[parent performBlock:^{
NSError *parentError;
BOOL parentSaved = [parent save:&parentError];
if (!parentSaved) {
NSLog(#“Error saving parent: %#“, parentError);
}
}];
}
The save of the UI context is very fast because it doesn’t write data to disk. It just pushes changes to its parent. And because parent is of private queue concurrency type and you do the save inside performBlock’s block, that save happens in background without blocking the main thread.
Now about different managed objects in different contexts from your example. As you discovered, you can’t set an object from one context to a property of an object in another context. You need to choose a context where you need to do the change. Then transfer NSManagedObjectID of one of the objects to the target context. Then create a managed object from ID using one of the context’s methods. And finally set this object to a property of another one.
Essentially you are on the right track, but missing a couple of key elements;
Firstly you will need to transfer test from your main context to the secondary - this is done in the following way;
//this is the object saved in your main managedObjectContext;
NSManagedObjectID *currentTest = test.objectID;
creating the secondary context for adding your related objects can be performed on a background thread. You can use and NSBlockOperation to do the secondary save and create the context at the same time.
here is a simple example using the standard person / address example wired to an IBAction
- (IBAction)button1Click:(id)sender {
NSError *saveError = nil;
// create instance of person to save in our primary context
Person *newParson = [[Person alloc]initIntoManagedObjectContext:self.mainContext];
newParson.name = #"Joe";
[self.mainContext save:&saveError];
//get the objectID of the Person saved in the main context
__block NSManagedObjectID *currentPersonid = newParson.objectID;
//we'll use an NSBlockOperation for the background processing and save
NSBlockOperation *addRelationships = [NSBlockOperation blockOperationWithBlock:^{
// create a second context
NSManagedObjectContext *secondContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[secondContext setPersistentStoreCoordinator:coordinator];
NSError *blockSaveError = nil;
/// find the person record in the second context
Person *differentContextPerson = (Person*)[secondContext objectWithID:currentPersonid];
Address *homeAddress = [[Address alloc]initIntoManagedObjectContext:secondContext];
homeAddress.address = #"2500 1st ave";
homeAddress.city = #"New York";
homeAddress.state = #"NY";
homeAddress.zipcode = #"12345";
Address *workAddress = [[Address alloc]initIntoManagedObjectContext:secondContext];
workAddress.address = #"100 home Ave";
workAddress.city = #"Newark";
homeAddress.state = #"NJ";
homeAddress.zipcode = #"45612";
[differentContextPerson addAddressObject:homeAddress];
[differentContextPerson addAddressObject:workAddress];
[secondContext save:&blockSaveError];
}];
[addRelationships start];
}
in the above initIntoManagedObjectContext is a simple helper method in the NSManagedObject subclass as follows;
- (id)initIntoManagedObjectContext:(NSManagedObjectContext *)context {
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Person" inManagedObjectContext:context];
self = [super initWithEntity:entity insertIntoManagedObjectContext:context];
return self;
}
An important note from Apple docs regarding NSBlockOperation:
You must create the managed context on the thread on which it will be used. If you use NSOperation, note that its init method is invoked on the same thread as the caller. You must not, therefore, create a managed object context for the queue in the queue’s init method, otherwise it is associated with the caller’s thread. Instead, you should create the context in main (for a serial queue) or start (for a concurrent queue).

Parent MOC get changes with empty data from child MOC

I'm stuck on this problem with CoreData and Parent-Child MOCs: when adding objects to child MOC, saving them and saving the parent MOC all the objects gets their attributes reset to defaultValue.
I pasted here the logs from the two MOCs, specifically are the "stringAttribute" and "date" attributes that in these log are reset.
I searched for this problem everywhere but i didn't find anything, I also looked at lots of implementation of Parent-Child MOCs but I can't figure out what I'm doing wrong.
Thanks in advance!
Here's the code snippets:
I add some NSManagedObject to the main context and then save with saveContext: method
// Another singleton method
- (void)anotherMethod
{
[...]
[self.managedObjectContext insertObject:managedObject];
NSError *error;
save = [self saveContext:&error];
[...]
}
// Database manager singleton method
- (BOOL)saveContext:(DKError *__autoreleasing *)error
{
__block BOOL save = NO;
__block NSError *internalError;
[self.managedObjectContext performBlockAndWait:^{
internalError = nil;
[self.managedObjectContext log]; // See log 1.1 below
save = [self.managedObjectContext save:&internalError];
if (!save) {
NSLog(#"Error saving data in main context");
} else {
[self.managedObjectContext.parentContext performBlock:^{
internalError = nil;
save = NO;
[self.managedObjectContext.parentContext log]; // See log 1.2 below
save = [self.managedObjectContext.parentContext save:&internalError];
if (!save) {
NSLog(#"Error saving data to disk!");
}
}];
}
}];
*error = [DKError errorWithNSError:internalError]; // Custom error class
return save;
}
Parent - Child contexts code
- (NSManagedObjectContext *)writerObjectContext
{
if (_writerObjectContext != nil)
return _writerObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_writerObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_writerObjectContext setPersistentStoreCoordinator:coordinator];
}
return _writerObjectContext;
}
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:[self writerObjectContext]];
return _managedObjectContext;
}
Log 1.1
Inserted objects:
{(
<Entity: 0x9595120> (entity: Entity; id: 0x9582d40 <x-coredata:///Entity/t24D0F98B-CB94-41D3-BEDD-79913454A9152> ; data: {
[...]
dateAttribute = "2013-07-12 10:36:31 +0000";
stringAttribute = ricercaEntity;
[...]
})
)}
Log 1.2
Inserted objects:
{(
<Entity: 0xb53ce80> (entity: Entity; id: 0x9582d40 <x-coredata:///Entity/t24D0F98B-CB94-41D3-BEDD-79913454A9152> ; data: {
[...]
dateAttribute = "2013-01-05 11:00:00 +0000";
stringAttribute = nil;
[...]
})
)}
UPDATE
I've should have mention that the managedObject added to the context is initialized with context nil. Then before calling saveContext: I check for existence of object.managedObjectContext and if it's nil then I'll set that as [self managedObjectContext], the one created with the method above. So either if the managedObject is created with nil context, or created with:
+ (id)newObjectForInsertion
{
return [[self alloc] initWithEntity:[self entityDescription] insertIntoManagedObjectContext:[DKDatabaseManager defaultManager].managedObjectContext];
}
the associated managedObjectContext is on the same queue (NSMainQueueConcurrencyType).
Otherwise if the managedObject is create with +newObjectForInsertion all of the saveContext: concurrency-chain return YES and all the changes are passed to parent context.
I don't know if it's a bug or the way CoreData should work.
Same problem on Apple Developer Forums:
https://devforums.apple.com/thread/174677?tstart=90
You should init context with concurrencyType:
context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
Also, set merge policy
[context setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
NSMergeByPropertyObjectTrumpMergePolicy
This policy merges conflicts between the persistent store’s version of
the object and the current in-memory version, giving priority to
in-memory changes. The merge occurs by individual property. For
properties that have been changed in both the external source and in
memory, the in-memory changes trump the external ones.
Btw, I found similar question: strange-behavior-when-using-child-parent-nsmanagedobjectcontext look at the accepted answer which uses notifications to merge.

Resources