Marko Zadravec
Marko Zadravec

Reputation: 8730

Getting and using NSManagedObject in different threads

I have problem with architecting my code. I have view that create and depend on model class. Model class is responsible for all calculations and logic. This mean that it has a lot of things to calculate, and getting object from Core data.

enter image description here

Regarding enter link description here, every NSManagedObject that is obtained in one thread, need to be used in same thread.

My problem is that on create object need to be obtained in different thread, because it take some time to build model, but after this, I need to obtain object and they properties in main thread from View (for example in cellForIndex...)

I know, I am not the only one with this design. How others solve this problem?


EDIT :

To concrete this with code.

Let say we have UIView object MyUIView and model object Model

MyUIView

 @interface MyUIView ()

 @property(nonatomic) Model * model;

 @end

 @implementation MyUIView

 - (void) createModel
 {
     // privateManagerContext is context manager created with 
     // NSPrivateQueueConcurrencyType, connected to persistent store
     // and sync with "main thread" manager context with 
     // NSManagedObjectContextDidSaveNotification and NSManagedObjectContextDidSaveNotification

     [self.model createWithManagadContext:privateManagerContext];
 }

 // ASSUME THAT THIS CODE IS CALLED AFTER 
 - (void) getNumberOfSomeProperties
 {
    int number  = [self.model getNumberOfProperties];
 }

 - (void) getProperties
 {
    NSArray *array = [self.model properties]
 }

 // OR WE HAVE TO TRIGGERED SOME LONG CALCULATION

 - (void) triggerLongCalculation
 {
     [self.model longCalculation];
 }

 - (void) afterNotifyModelIsCompletedCalculating
 {
     [self doSomeWork];
     [self getProperties];
     ....
 }
 @end

Model

 @interface Model ()

 @property(nonatomic) NSArray * cashedProperties;

 @end

 @implementation MyUIView

 - (void) createWithManagadContext:(NSManagedObjectContext *) privateManagerContext
 {
     dispatch_async(self.model_queue, ^(void){

         // Here we fetch objects and do some calculations
         [self populateModel];

         /// Model is complete
         [Notifications notifyModelIsCompletedCreating:self];
     });
 }


 - (void) longCalculation
 {
     dispatch_async(self.model_queue, ^(void){
        // NO CORE DATA FETCH INVOLVED
        [Notifications notifyModelIsCompletedCalculating:self];
    });
 }

 - (int) getNumberOfProperties
 {
    return self.cashedProperties.count;
 }

 - (NSArray) properties
 {
    NSMutableArray * a = [[NSMutableArray alloc]init];
    for (Property * p in self.cashedProperties) {
        [a addObject:p.name];
    }

    return a;
 }

 @end

So in this hypothetical classes, how would you handle all NSManagedObject and NSManagedObjectContext ?

EDIT 2 :


I am using pattern where I create two managed object context in appdelegate, one private and one main, and create sync between them.

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

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        __managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        [__managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return __managedObjectContext;
}

- (NSManagedObjectContext * ) privateQueueContext
{
    if (_privateQueueContext != nil) {
        return _privateQueueContext;
    }

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


- (id)init
{
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self
                                               selector:@selector(contextDidSavePrivateQueueContext:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:[self privateQueueContext]];
        [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(contextDidSaveMainQueueContext:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                                   object:[self managedObjectContext]];
    }
    return self;
}

#pragma mark - Notifications

- (void)contextDidSavePrivateQueueContext:(NSNotification *)notification
{
    @synchronized(self) {
        [self.managedObjectContext performBlock:^{

            NSArray* objects = [notification.userInfo valueForKey:NSUpdatedObjectsKey];
            for (NSManagedObject* obj in objects) {
                NSManagedObject* mainThreadObject = [self.managedObjectContext objectWithID:obj.objectID];
                [mainThreadObject willAccessValueForKey:nil];
            }

            [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

- (void)contextDidSaveMainQueueContext:(NSNotification *)notification
{
    @synchronized(self) {
        [self.privateQueueContext performBlock:^{

            NSArray* objects = [notification.userInfo valueForKey:NSUpdatedObjectsKey];
            for (NSManagedObject* obj in objects) {
                NSManagedObject* mainThreadObject = [self.privateQueueContext objectWithID:obj.objectID];
                [mainThreadObject willAccessValueForKey:nil];
            }

            [self.privateQueueContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

Upvotes: 0

Views: 524

Answers (1)

ubiAle
ubiAle

Reputation: 445

You could use two different NSManagedObjectContext with parent/child configuration. The parent for the UI, in the main queue, the child for heavy work in the private queue. Once the child context finishes to perform his heavy work would save, its changes are then propagated to the main context. You could use in the case you're using a table view in your view controller a NSFetchedResultsController that observe on the main context. Once the main context receives and merge the changes from his child context, the NSFetchedResultsController would update the UI accordingly, as long as its delegate methods are implemented.

If you don't use NSFRC you could register your main context for notifications "name:NSManagedObjectContextObjectsDidChangeNotification" or "name:NSManagedObjectContextObjectsDidSaveNotification" and see what objects have been added/deleted/updated and update the UI consequently.

If you use thread confinement instead (made obsolete by Apple in the last years) of parent/child you may want to pass the objectID between threads and fetch the object in the context you need it as NSManagedObjects are not thread-safe.

Code:

First I wouldn't have a reference of your model in the view. View should be just dumb and expose outlets or methods to be populated. I would have a ViewController communicating with the model and the view.

Lets say you have a util method to create a worker context (as child context of your parent in the main queue) every time you need to perform a heavy job.

func newWorkerContext() -> NSManagedObjectContext {
    let workerContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
    workerContext.parentContext = self.mainContext
    return workerContext
}

In your model you would have

//Lets say you have a reference to your workerContext
func longCalculation() {
    workerContext.performBlock({ () -> Void in
          //Heavy process of information
          //Once finished you save the worker context and changes are propagated to the parent context    
    })

}

In your ViewController you would have

class MyViewController: UIViewController {

   func viewDidLoad() {
      super.viewDidLoad()

      NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(handleDataModelChange(_:)), name: NSManagedObjectContextObjectsDidChangeNotification, object: REFERENCE_TO_MAIN_CONTEXT)
   }

  func deinit {   
      NSNotificationCenter.defaultCenter().removeObserver(self, name: NSManagedObjectContextObjectsDidChangeNotification, object: REFERENCE_TO_MAIN_CONTEXT)  
  }

func handleDataModelChange(notification: NSNotification) {

    //Check changes are relevant for the UI you are updating and if so update.
    if let changedObjects = changes[NSUpdatedObjectsKey] as? NSSet {
    }
    if let insertedObjects = changes[NSInsertedObjectsKey] as? NSSet {
    }
    if let deletedObjects = changes[NSDeletedObjectsKey] as? NSSet {
    }
}
}

Remember that to persist the changes in the persistence store you have to save on the main context. It's a simple example but I hoped it gives you the idea on what to do now.

Upvotes: 1

Related Questions