3

I've got an app design question that I'm hoping someone can help with.

Let's take a very simple setup: Core Data app for displaying news items from a server.

  • Main thread / UI has a managed object context that's used by all the view controllers to display the data.

  • An NSOperation runs in the background checking the server, with it's own context, on the same persistent store.

I want to merge the changes in the background context so I use NSManagedObjectContextObjectsDidChangeNotification.

According to the Apple docs:

Several system frameworks use Core Data internally. If you register to receive these notifications from all contexts (by passing nil as the object parameter to an addObserver… method), then you may receive unexpected notifications that are difficult to handle.

So, I want to filter my notifications merged in the main thread MOC to just those changes coming from the background operation MOC.

What's the cleanest way to get/maintain a reference to the background operation MOC so that I have something to plug into the addObserver method and the notifications are properly filtered? I can think of a lot of ways that involve a lot of coupling but they all seem like a hack.

Any suggestions or ideas? How are others handling this?

Hunter
  • 4,343
  • 5
  • 42
  • 44
  • So, the primary thrust of my question is how to handle this with the minimum amount of coupling between components. For instance, front-end stuff like view controllers shouldn't have to know anything about back-end stuff like NSOperations. – Hunter May 02 '11 at 19:15

4 Answers4

4

Here's how it works in my app:

// should be executed on a background thread
- (void)saveWorkerContext {
    if ([_workerContext hasChanges]) {
        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];    
        [nc addObserver:self selector:@selector(workerContextDidSave:)
                   name:NSManagedObjectContextDidSaveNotification object:_workerContext];

        NSError *error;
        if (![_workerContext save:&error]) {
            NSAssert(NO, @"Error on save: %@", error);
        }

        [nc removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_workerContext];
    }
}

- (void)workerContextDidSave:(NSNotification *)notification {
    if (_mainContext) {
        [_mainContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) 
                                       withObject:notification waitUntilDone:NO];
    }
}
sgosha
  • 1,299
  • 12
  • 19
  • So say we have an NSOperation type setup to handle the background processing, you're putting the code to handle the merge there and then it somehow (i.e. passed in or whatever) has a reference to the main thread MOC (_mainContext)? My goal/concern is to do this all in away that minimizes coupling between components but maybe I just need to pass that main MOC into the NSOperation so it can handle things like the above. Am I understanding you correctly? – Hunter May 02 '11 at 19:00
  • I use one shared instance of a custom NSObject subclass that is available from any place inside the app. `_mainContext` and `_workerContext` are its member variables. you can do that too if you wish. in that case NSOperation's target will be that shared instance of NSObject subclass. Another solution is provide access to main thread MOC from NSOperation's selector via a selector on NSOperation's target or any shared instance (appDelegate?) You can also pass a reference to the main thread MOC as a parameter of NSOperation. – sgosha May 02 '11 at 19:21
2

--- Revised Answer ---

Using NSFectchedResultsController appears to be your best bet. It will inform it's delegate when changes to it's MOC effect it's results. This negates the need for your view controller to know about or directly observe events from your background MOC.

Here's the pattern I'd use with NSOperation worker processes.

Store the background MOC in a NSOperationQueue subclass with a maxConcurrentOperationCount of 1. This ensures the operations will occur serially.

  • Subclass NSOperationQueue
  • Add a property for the background MOC
  • Implement the NSOperationQueue's MOC getter to lazily create the background MOC from the Persistent store and register the class responsible for the merging the context with the background MOC did save notification (Typically your AppDelegate or singleton)
  • Unregister observations for the class and clean up the background MOC in dealoc

Create your NSOperationQueue subclass.

Before adding operations, provision them with the background MOC from the queue's background MOC property. When scheduled, your operation will perform work with the background MOC and save.

Perform the merge when the did save notification comes in on the observing class. After the merge, each fetched results controller using the foreground MOC will notify it's delegate when any changes had an impact on it results. This includes additions or deletions from the background MOC merge.

Scott Ahten
  • 1,141
  • 7
  • 15
  • Note that, in this case, only your NSOperationQueue subclass knows about the class handling the did save notification. Your NSOperation subclasses are handed an already configured MOC and do not interact with other instances beyond what is necessary to perform it's particular background task. – Scott Ahten May 04 '11 at 05:54
1

I am not sure I fully understand your question: if you have just one background thread associated to one particular MOC you want to track, then there is nothing special to do:use a property to maintain a reference to the MOC. You handle this as usual, as shown in the following code snippet.

// create a new MOC
self.backgroundMOC = ...; 

// register to receive notifications
[[NSNotificationCenter defaultCenter] addObserver:self
                                      selselector:@selector(contextDidSave:) 
                                             name:NSManagedObjectContextDidSaveNotification
                                           object:self.backgroundMOC];


// pass backgroundMOC to your background thread
// and handle notifications here
- (void)contextDidSave:(NSNotification *)notification
{
        NSManagedObjectContext *MOC = (NSManagedObjectContext *) [notification object];
        if([MOC isEqual:self.backgroundMOC])
           [managedObjectContext mergeChangesFromContextDidSaveNotification:notification];

}
Massimo Cafaro
  • 25,429
  • 15
  • 79
  • 93
  • If the background MOC is created elsewhere, say in an NSOperation that is designed to handle background processing, when your view controller goes to listen for save notifications, how does it get the reference to that background MOC that is an NSOperation elsewhere? Where does the above code live? In said NSOperation or in a main-thread object like a VC or elsewhere? – Hunter May 02 '11 at 18:57
  • As shown in the code, you can have a property for this in your main view controller. Another solution is to create and store it in your app delegate: you simply get a reference to it from the delegate. Still another option is to use a shared class taking care of it. The important thing to note here is that in the contextDidSave: method you may then filter notifications from the background MOC as you require: the code shows how to check if the MOC associated to the notification object actually is the background MOC. – Massimo Cafaro May 02 '11 at 21:02
1

If you don't want to have any communication between the threads/operations that hold the context, then the only way to identify if a context generating a notification belongs to you would be to check its persistent store url.

Only your context would have your store URLs. The API URLs will have system stores or in-memory stores.

Normally, of course, you do communicate between processes and can just pass the object references for identification purposes.

TechZen
  • 64,370
  • 15
  • 118
  • 145
  • Any opinions on what should own background MOCs? Units of work like NSOperation or something like the app delegate? Also, is it not true that if a MOC is created on the main thread, it is tied into the main run loop and thus it's better to create background MOCs on the threads where they will be used (i.e. the docs say "You should not, therefore, initialize a context on one thread then pass it to a different thread."). – Hunter May 02 '11 at 21:46
  • If you use NSOperation, then the context should be owned by the operation object itself. Operations are like little self-contained programs so you design them as such. Ideally, notifications are the only thing they should pass around. – TechZen May 03 '11 at 12:48
  • Right... but that does make referencing the background MOC that much harder... That was sort of the crux of the original question. If you're creating a MOC in an NSOperation, what's the best way to merge those changes into the main MOC with the least coupling between components. – Hunter May 04 '11 at 01:16