lhunath / UbiquityStoreManager

Implements Core Data + iCloud, deals with all the nasty stuff and gives you a clean API.
http://lhunath.github.io/UbiquityStoreManager
Apache License 2.0
391 stars 37 forks source link

Best practice for keeping strong refs to NSManagedObjects #36

Closed measuredweighed closed 10 years ago

measuredweighed commented 10 years ago

I'm currently looking to implement iCloud sync in an existing application that has, up until this point, passed around an instance of my main NSManagedObjectContext to various controllers and models.

Given that the UbiquityStoreManager documentation warns that my main MOC could become unavailable at any time, I've taken steps to instead refer to my AppDelegate's MOC property and to fail gracefully if it is unavailable. That seems clear, however I'm confused as to best practice when referencing NSManagedObjects.

As a basic example: I pass NSManagedObject references to my detail view controllers for editing purposes. I'm aware that it's generally a bad idea to shift NSManagedObjects between NSManagedObjectContexts, so what's the best way of handling this? Should I be looking up the NSManagedObject via it's objectID in the current main MOC each time I try to access it within the detail view?

lhunath commented 10 years ago

So, this is not really a USM issue, but an important Core Data concept to get right, especially when using USM.

To understand how best to deal with these issues, it's important that we understand what exactly the scope is within which each type of relevant object is valid:

So what does that mean for passing these things around?

Here's what I'd recommend:

  1. Do not hold on to your coordinator. When you get it in didLoadStoreForCoordinator:coordinator, use it to make your primary context and then forget about it.
  2. Hold onto your primary context with an instance variable in your application's delegate. Your primary context should be created in didLoadStoreForCoordinator: using the coordinator you are given. I'd recommend making my primary context using NSPrivateQueueConcurrencyType, but simpler applications may prefer NSMainQueueConcurrencyType. Be certain that private contexts are always accessed using their performBlock methods and that main contexts are only accessed from the methods running in the main thread. If your primary context is private, you'll either want to create a child main context or create a new main context each time a UI event occurs. Creating contexts is cheap: you can do this whenever you need to do something new without too much of a performance worry. '''Be sure to unset''' all your contexts in willLoadStoreIsCloud:. You may want to first save your primary context, if you don't do so explicitly whenever you change it. nil your context instance variables and make sure your UI doesn't crash or bug out when you have no context. Some prefer to show an activity indicator of sorts so the user knows the UI is temporarily unavailable.
  3. Your NSManagedObjects should be only local variables. Make sure not to pass them between threads (so be careful about passing them into blocks!). If you're going to pass them as arguments to method calls, make sure there's a clear contract between these methods about what context to use when working with the object. Keep in mind that object.context may be nil, so you can't always rely on getting the context that created the object from there. Also keep in mind that it's clear who's going to be saving the context after the object's been modified. Personally, I add this information to my method names (inContext vs saveInContext):

    [delegate signInAsUser:[self selectedUserInContext:context] saveInContext:context
      usingMasterPassword:self.passwordField.text];
  4. When you want to pass objects between threads or you want to hold onto them (eg. "the active user"), you should use NSManagedObjectIDs. More specifically, you should use '''permanent''' object ids (if you want to make sure whether your object has a permanent object.objectID, you could test its .isTemporaryID. To get a permanent ID, you'll need to save the object. You may also look into [context obtainPermanentIDsForObjects:@[ object ] error:&error). When you want to get the object for your ID, you can use your context's existingObjectWithID:objectID error:&error. When storing IDs, I tend to use the following pattern: I have a private instance variable for the objectID, a getter method for the object which takes a context and a setter method:

    - (MPUserEntity *)selectedUserInContext:(NSManagedObjectContext *)context {
    
       if (!_selectedUserOID)
           return nil;
    
       NSError *error;
       MPUserEntity *selectedUser = (MPUserEntity *)[context existingObjectWithID:_selectedUserOID error:&error];
       if (!selectedUser) {
           err(@"Failed to retrieve selected user: %@", error);
           _selectedUserOID = nil;
       }
    
       return selectedUser;
    }
    
    - (BOOL)setSelectedUser:(MPUserEntity *)selectedUser {
    
       if ([_selectedUserOID isEqual:selectedUser.objectID])
           return NO;
    
       NSError *error = nil;
       if (selectedUser.objectID.isTemporaryID &&
           ![selectedUser.managedObjectContext obtainPermanentIDsForObjects:@[ selectedUser ] error:&error]) {
           err(@"Failed to obtain a permanent object ID after setting selected user: %@", error);
           _selectedUserOID = nil;
       }
    
       _selectedUserOID = selectedUser.objectID;
       return YES;
    }

Hopefully this makes things a bit clear for you. Let me know if you have any comments or need more details.