Question

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.

Was it helpful?

Solution

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.

OTHER TIPS

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

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top