0

I am making an app which parses XML returned by links and saves that data using Core Data. The problem is that in order for the UI to remain responsive I must do all my parsing in the background thread. After googling for a while I have set up two contexts in my AppDelegate, one for handling the background parsing and one for loading the UI.

First: Is this the right way to set up two contexts for my situation?

//In AppDelegate.m

//This one is for parsing in the background
- (NSManagedObjectContext *)backgroundManagedObjectContext {
    if (_backgroundManagedObjectContext != nil) {
         return _backgroundManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        _backgroundManagedObjectContext.persistentStoreCoordinator = coordinator;
    }

    return _backgroundManagedObjectContext;
}

//This one is for updating the UI
- (NSManagedObjectContext *)mainManagedObjectContext {
    if (_mainManagedObjectContext != nil) {
        return _mainManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
         _mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        _mainManagedObjectContext.persistentStoreCoordinator = coordinator;
        _mainManagedObjectContext.parentContext = self.backgroundManagedObjectContext];
    }

    return _mainManagedObjectContext;
}

Second: Would this be the proper way to call the individual sync and load methods?

I would then have calls to my sync methods (for background) as so:

[self.appDelegate.backgroundManagedObjectContext performBlock:^{
    [self syncData];
}

And calls to to my load methods (for UI) as so

[self.appDelegate.mainManagedObjectContext performBlock:^{
    [self loadData];
}

Third: My Core Data is centered around one entity (User) which all other entities are connected to. Do I have to create a local variable and fetch it every time I want to use it, or is there a better way so the NSManagedObject is not shared across the two threads?

Fourth: If I save the parent background context, will the child context (UI) automatically be updated or is there a specific method I need to call?

carloabelli
  • 4,289
  • 3
  • 43
  • 70
  • Is it possible for `_backgroundManagedObjectContext` to be `nil` when `mainManagedObjectContext` is called? – Aaron Brager Dec 10 '13 at 17:52
  • @AaronBrager That is the reason I am asking if this would be a correct setup. Should I check for that in the `- (NSManagedObjectContext *)mainManagedObjectContext` method? – carloabelli Dec 10 '13 at 19:22
  • Or you could use `[self backgroundManagedObjectContext]` instead of `_ _backgroundManagedObjectContext`, which would instantiate it if it hasn't been instantiated. – Aaron Brager Dec 10 '13 at 19:30
  • @AaronBrager I edited the question to include your suggestion. Do you have an answer to any of my more general questions? – carloabelli Dec 10 '13 at 23:51

1 Answers1

1

With Core Data, it seems the best way to learn is to compare against what has worked. I'll post some code that deals with situations similar to yours.

First: Is this the right way to set up two contexts for my situation?

This is what I have in my AppDelegate right now:

- (NSManagedObjectContext *)auxiliaryManagedObjectContext {
    NSManagedObjectContext *managedObjectContext = nil;

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        managedObjectContext = [[NSManagedObjectContext alloc] init];
        [managedObjectContext setPersistentStoreCoordinator:coordinator];
        [managedObjectContext setUndoManager:nil];
    }

    return managedObjectContext;
}

- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _managedObjectContext = [[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }

    return _managedObjectContext;
}

Second: Would this be the proper way to call the individual sync and load methods?

This really depends on what is happening in these methods. When you are working with a context in the background, you should send setup a notification that will trigger when a change is made. In this method, you then merge the changes from the background context into the UI context:

dispatch_queue_t background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long) NULL);
dispatch_async(background, ^{


    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *backgroundManagedObjectContext = [appDelegate auxiliaryManagedObjectContext];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(mergeContexts:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:backgroundManagedObjectContext];

    NSManagedObject *threadSafeManagedObject =
    [backgroundManagedObjectContext objectWithID:self.currentManagedObject.objectID];

    NSManagedObject *insertedThreadSafeManagedObject = [NSEntityDescription insertNewObjectForEntityForName:@"Entity" inManagedObjectContext:backgroundManagedObjectContext];

    NSError *error;
    // Will call the mergeContexts: selector registered above
    if(![backgroundManagedObjectContext save:&error]) {
        NSLog(@"Error! %@", error);
    }
});

- (void)mergeContexts:(NSNotification *)notification {
    SEL selector = @selector(mergeChangesFromContextDidSaveNotification:);
    [self.managedObjectContext performSelectorOnMainThread:selector withObject:notification waitUntilDone:YES];
}

In the mergeContexts: method, the background context is merged with your UI context. It is very important that a context that starts in the background, stays in the background and vice-versa. Additionally, managed objects are NOT thread safe, so to use one from the UI thread in the background thread, you must transfer it to the background context view the objectID as in the example above.

Third: My Core Data is centered around one entity (User) which all other entities are connected to. Do I have to create a local variable and fetch it every time I want to use it, or is there a better way so the NSManagedObject is not shared across the two threads?

The description above should have answered this question.

Fourth: If I save the parent background context, will the child context (UI) automatically be updated or is there a specific method I need to call?

That would be the mergeContexts: method in the above example. Let me know if this makes sense.

EDIT 1: initWithConcurrencyType

As you mentioned, there are the NSMainQueueConcurrencyType and NSPrivateQueueConcurrencyType used during initialization of managed object contexts that handle this back and forth exchange between background and UI contexts.

The AppDelegate initializers would be practically the same. The only difference would be the init statements:

// Background context
managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];

// Main thread context
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

You would then grab the background context–in this situation from your AppDelegate–and use the background thread to begin the work and finish the work on the main thread:

AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *backgroundManagedObjectContext = [appDelegate auxiliaryManagedObjectContext];

[backgroundManagedObjectContext performBlock:^{
    // Do work
    [self.managedObjectContext performBlock:^{
        // merge work into main context
    }];
}]; 

It is important to state the same thread safety rules apply (e.g. background objects must be transferred to the main thread view its objectID). To pass the work from your background thread to the main thread, you should call the main thread context's performBlock inside the background context's perform block. Though the convenience of merging contexts still lies within the notification arrangement regardless of how you initialize your contexts.

EDIT 2: Child/Parent Contexts

Here is a parent/child example from Correct implementation of parent/child NSManagedObjectContext:

- (void)saveContexts {
    [childContext performBlock:^{
        NSError *childError = nil;
        if ([childContext save:&childError) {
            [parentContext performBlock:^{
                NSError *parentError = nil;
                if (![parentContext save:&parentError]) {
                    NSLog(@"Error saving parent");
                }
            }];
        } else {
            NSLog(@"Error saving child");
        }
    }];
}

In our case, when initializing the child context, set its parent to your UI context:

[backgroundManagedObjectContext setParentContext:_managedObjectContext];
Community
  • 1
  • 1
Chris
  • 1,663
  • 1
  • 15
  • 19
  • Thank you for your detailed answer! It makes sense, but I believe I read somewhere that `NSPrivateQueueConcurrencyType` and `NSMainQueueConcurrencyType` were supposed to replace this. For my situation would it be better to use your example code or work with these? – carloabelli Dec 11 '13 at 00:38
  • Also if I have one entity which contains all the other entities (via relationships) can I create two instances of it in my AppDelegate (`self.appDelegate.user` & `self.appDelegate.backgroundUser`) so that all my ViewControllers have access to this user (especially because I am using `NSXMLParser`s as well? And make sure to only use each in their respective thread. – carloabelli Dec 11 '13 at 00:50
  • There really is not a good reason I can think of to have two global versions of your user. When you want to use the current user in the background use `NSManagedObject *threadSafeManagedObject = [backgroundManagedObjectContext objectWithID:self.currentManagedObject.objectID];` register for the `NSManagedObjectContextDidSaveNotification` make your desired changes then save and merge the changes into your main context. IMHO, maintaining two global versions is a headache that is not worth the effort. – Chris Dec 11 '13 at 01:00
  • As a note, maintaining any object's state throughout an application is already a challenge. Maintaining two is even more. And maintaining two on two separate threads is asking for trouble. Even the most experienced multi-thread programmers have trouble with ensuring objects are thread safe. – Chris Dec 11 '13 at 01:03
  • So basically I should only store the user (NSManagedObject)'s ID and then just get it every time depending on which thread I am in. Also I read somewhere about setting one as a parent which should automatically update without having to register the notification. Would this be an easier route or is this incorrect? – carloabelli Dec 11 '13 at 01:08
  • Once configured, it seems that would be an easier route. I'm honestly not familiar with the process but it should not stray too far away from what we have discussed. – Chris Dec 11 '13 at 01:14
  • `So basically I should only store the user (NSManagedObject)'s ID` the users ID is its objectID, which is automatically stored when you insert it into a context. – Chris Dec 11 '13 at 01:17
  • To rephrase: The method you used to access the user was by accessing it from a global variable. Should I make the variable for the main thread global in the AppDelegate and use its objectID for the background thread, or should I just store an objectID and fetch the user every time. – carloabelli Dec 11 '13 at 01:22
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/42906/discussion-between-chris-and-cabellicar123) – Chris Dec 11 '13 at 01:23