How to best handle CoreData + iOS State Restoration?
Asked Answered
G

6

25

I'm trying to add iOS 6 State Restoration to an app that I'm just about finished with. It's an app where the model mostly comes from CoreData.

As recommended, I'm using the "pass the baton" approach to moving managed object contexts between View Controllers - I create the MOC in my App Delegate, pass it to the first View Controller, which passes it to the second in prepareForSegue:, which passes it to the third in prepareForSegue:, etc.

This doesn't seem to jive very well with State Restoration. The only thing I can think of to do is to retrieve the MOC from my App Delegate directly in an implementation of viewControllerWithRestorationIdentifierPath:coder:. In fact, it appears that the Apple developers did something similar when watching the WWDC session.

Is this the best/only way? Does State Restoration effectively break Pass-The-Baton, at least for view controllers that are restored?

Goodard answered 31/7, 2013 at 22:8 Comment(4)
Did you find a solution to this? It's a good question and I am also thinking about how to best solve this.Milzie
Anton - not really. I just started thinking of it a "special case" and went with the original idea of referencing the App Delegate directly. I'll leave the question open to get a feel for what other people are trying.Goodard
@Matthias-Bauch I haven't looked at this stuff in a while, but I believe either in 2014 there was a WWDC session called "what's new in state restoration" that had some new features, and it sounded like that might help here.Goodard
NSManagedObjectContext conforms to the NSCoding protocol. Granted I haven't attempted this, but it seems like you should be able to encode it and then decode it in all restorable VCs.Labored
E
9

To become familiar with state restoration I highly recommend the WWDC 2013 session What's New in State Restoration. While state restoration was introduced a year earlier in iOS 6, iOS 7 brought some notable changes.

Passing it forward

Using the "pass the baton" approach, at some point a root NSManagedObjectContext is created and an NSPersistentStoreCoordinator is attached. The context is passed to a view controller and subsequent child view controllers are in turn passed that root context or a child context.

For example, when the user launches the application the root NSManagedObjectContext is created and passed in to the root view controller, which manages an NSFetchedResultsController. When the user selects an item in the view controller a new detail view controller is created and an NSManagedObjectContext instance is passed in.

Passing the Managed Object Context Baton

Saving and Restoring State

State restoration changes this in ways that are significant to applications using Core Data in view controllers. If the user is on the detail view controller and sends the application to the background the system creates a restoration archive with information useful for reconstructing the state visible when they left. Information about the entire chain of view controllers is written out, and when the application is relaunched this is used to reconstruct the state.

State Restoration

When this happens it does not use any custom initializers, segues, etc. The UIStateRestoring protocol defines methods used for encoding and decoding state which allow for some degree of customization. Objects that conform to NSCoding can be stored in restoration archives and in iOS 7 state restoration was extended to model objects and data sources.

State restoration is intended to store only the information that is required to reconstruct the visible state of the application. For a Core Data application this means storing the information needed to locate the object in the correct persistent store.

On the surface, this seems simple. In the case of a view controller managing an NSFetchedResultsController this may mean storing the predicate and sort descriptors. For a detail view controller that displays or edits a single managed object the URI representation of the managed object would be added to the state restoration archive:

- (void) encodeRestorableStateWithCoder:(NSCoder *)coder {
    NSManagedObjectID   *objectID   = [[self managedObject] objectID];

    [coder encodeObject:[objectID URIRepresentation] forKey:kManagedObjectKeyPath];
    [super encodeRestorableStateWithCoder:coder];
}

When the state is restored the UIStateRestoring method -decodeRestorableStateWithCoder: is called to restore the object from the archived information:

  1. Decode the URI from the restoration archive.
  2. Get a managed object ID for the URI from the persistent store coordinator
  3. Get a managed object instance from the managed object context for that managed object ID

For example:

- (void) decodeRestorableStateWithCoder:(NSCoder *)coder {
    NSURL               *objectURI  = nil;
    NSManagedObjectID   *objectID   = nil;
    NSPersistentStoreCoordinator    *coordinator    = [[self managedObjectContext] persistentStoreCoordinator];

    objectURI = [coder decodeObjectForKey:kManagedObjectKeyPath];
    objectID = [coordinator managedObjectIDForURIRepresentation:objectURI];
    [[self managedObjectContext] performBlock:^{
        NSManagedObject *object = [self managedObjectContext] objectWithID:objectID];
        [NSOperationQueue mainQueue] addOperationWithBlock:^{
            [self setManagedObject:object];
        }];
    }]; 
}

And this is where things become more complicated. At the point in the application life cycle where -decodeRestorableStateWithCoder: is called the view controller will need the correct NSManagedObjectContext.

Pass the Baton vs. State Restoration: FIGHT!

With the "pass the baton" approach the view controller was instantiated as a result of user interaction, and a managed object context was passed in. That managed object context was connected to a parent context or persistent store coordinator.

During state restoration that does not happen. If you look at the illustrations of what happens during "pass the baton" vs. state restoration they may look very similar - and they are. During state restoration data is passed along - the NSCoder instance that represents an interface to the restoration archive.

Unfortunately the NSManagedObjectContext information we require can't be stored as part of the restoration archive. NSManagedObjectContext does conform to NSCoding, however the important parts do not. NSPersistentStoreCoordinator does not, so it will not be persisted. Curiously, the parentContext property of an NSManagedObjectContext also will not (I would strongly suggest filing a radar on this). Storing the URLs of specific NSPersistentStore instances and recreating an NSPersistentStoreCoordinator in each view controller may seem like an attractive option but the result will be a different coordinator for each view controller - which can quickly lead to disaster.

So while state restoration can provide the information needed to locate entities in an NSManagedObjectContext, it can't directly provide what is needed to recreate the context itself.

So what next?

Ultimately what is needed in a view controller's -decodeRestorableStateWithCoder: is an instance of NSManagedObjectContext that has the same parentage that it did when state was encoded. It should have the same structure of ancestor contexts and persistent stores.

State restoration begins in the UIApplicationDelegate, where several delegate methods are invoked as part of the restoration process (-application:willFinishLaunchingWithOptions:, -application:shouldRestoreApplicationState:, -didDecodeRestorableStateWithCoder:, -application:viewControllerWithRestorationIdentifierPath:coder:). Each one of these is an opportunity to customize the restoration process from the beginning and pass information along - such as attaching an NSManagedObjectContext instance as an associated object reference to the NSCoder used for restoration.

If the application delegate object is responsible for creating the root context that object could be pushed down throughout the view controller chain once the launch process is complete (with or without state restoration). Each view controller would pass the appropriate NSManagedObjectContext instance to it's child view controllers:

@implementation UIViewController (CoreData)

- (void) setManagedObjectContext:(NSManagedObjectContext *)context {
    [[self childViewControllers] makeObjectsPerformSelector:_cmd withObject:context];
}

@end

And each view controller that provided it's own implementation would create a child context of it's own. This has other advantages - any approach that has the users of a managed object context react to it changing makes it easier to create the context asynchronously. Creating a context itself is fast and lightweight, but adding the persistent stores to the root context is potentially very expensive and should not be allowed to run on the main queue. Many applications do this on the main queue in an application delegate method and end up being killed by the OS when opening the files of the store takes too long or a migration is required. Adding the persistent store on another thread and then sending the context to the objects that use it when it's ready can help prevent these kinds of problems.

Another approach may be to leverage the responder chain in the view controller. During state restoration the view controller could walk the responder chain to find the next NSManagedObjectContext up the chain, create a child context, and use that. Implementing this using an informal protocol is simple, and results in a solution that is flexible and adaptable.

The default implementation of the informal protocol would walk further up the responder chain:

@implementation UIResponder (CoreData)

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

    if ([self nextResponder] != nil){
        if ([[self nextResponder] respondsToSelector:@selector(managedObjectContext)]){
            result = [[self nextResponder] managedObjectContext];
        }
    }
    return result;
}

@end

And any object in the responder chain can implement -managedObjectContext to provide an alternate implementation. This includes the application delegate, which does participate in the responder chain. Using the informal protocol above, if a view or view controller calls -managedObjectContext the default implementation would go all the way to the application delegate to return a result unless some other object along the way provided a non-nil result.

You also have the option of using restoration class factories with state restoration to reconstruct the chain of managed object contexts during restoration.

These solutions are not appropriate for every application or situation, only you can decide what will work for you.

Emblematize answered 7/5, 2015 at 2:50 Comment(1)
It's a cool idea to use the responder chain but in practice I don't think it'll work. The reason is often you need the context in viewDidLoad or in viewWillAppear and there is no nextResponder yet in either of those cases. The targetViewControllerForAction from Apple's AdaptivePhotos sample might be a good alternative though, because the parentViewController is valid in viewDidLoad (when loaded from storyboard) and is always valid in viewWillAppear. You would need to set the context on the rootViewController in the app delegate though.Submiss
L
3

I think the best way to handle this would be to encode the MOC in:

- (void)encodeRestorableStateWithCoder:(NSCoder *)coder

and then decode when it's restored via:

- (void)decodeRestorableStateWithCoder:(NSCoder *)coder

This should retain the pass the baton approach between state restores.

Bear in mind, every VC that uses a MOC should implement this if you're going with this approach.

To expand slightly, utilize the + (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder method to initialize your VC, then the MOC should be decoded via the method mentioned above and you should be all set.

This should hopefully provide enough information to get you going as far as encoding and decoding any information you want to recover when restoring.

Labored answered 5/5, 2015 at 14:12 Comment(3)
NSManagedObjectContext does indeed support NSCoding. That actually sounds really weird. I'm not sure what it does. I'll have to check if that's helpful, tomorrow morning.Tetrapod
Unfortunately it is as I expected, archiving the context persists a couple basic values (i.e. concurrencyType, stalenessInterval, name, mergePolicy, ...), but it doesn't persist the whole stack. I honestly don't know how that would be possible anyway, because things like parentContexts need to be shared between different childContexts. The CoreData stack is basically an object graph in itself. So archiving the context just moves the problem to a different level, because I still have to figure out if and which persistentStoreCoordinator or parentContext the unarchived context should have.Tetrapod
Ha! UIApplicationDelegate has a method application(application:viewControllerWithRestorationIdentifierPath:coder:) where I can create the viewControllers centrally. Since that should be called in the correct order, I should be able to keep a reference to each individual viewController. Now I just have to store instructions how to recreate the context hierarchy. e.g. "Get the parentContext from viewController with restoration identifier Foo.Bar.Baz" Combining that with archiving the context does actually sound doable.Tetrapod
S
2

I have not done a ton with state restore but I would think along these lines:

  • Does the app delegate get woken up first? Is there an opportunity for the app delegate to walk the view controllers?

  • Can the view controller pause while it waits for the AppDelegate to give it the context?

Sounds like State Restoration might be a special case but I would explore the option of making the view controllers smart enough to wait for the MOC to appear before asking for data. Maybe even having a roll back state in the view controllers where they step back to a place where the view controller can wait for the context.

Summersault answered 14/1, 2014 at 19:2 Comment(0)
S
0

Subclass NSPersistentContainer (as the docs state); adopt the UIStateRestoring protocol so it can be registered with state restoration which allows the pointer to be accessible during restoration methods, the object itself isn't actually encoded in the archive. Call UIApplication's registerObjectForStateRestoration inside the lazy persistentContainer getter. Also as the docs state pass the persistentContainer to the view controllers in application:willFinishLaunching and prepareForSegue, not just the context. For view controllers that cannot get the container passed at willFinishLaunching, encode the persistentContainer and the object's URI in the VC's encodeRestorableStateWithCoder. For show (i.e. push) VCs that are pushed on the master's navigation stack, use the restorationClass technique and UIViewController class method viewControllerWithRestorationIdentifierPath and decode the persistentContainer and then using existingObjectWithID return nil if the object no longer exists which prevents the VC from being created, if it exists then init the VC using the encoded storyboard and the object. In the case of show detail VCs that are always created irrespective of the object existing there is no need to encode the persistentContainer and no need for the restoration class design, just implement the app delegate's application:viewControllerWithRestorationIdentifierPath: and use the app delegate's persistentContainer and set the object on the detail view controller from the initial storyboard (capture it in a property in application:willFinishedLaunching and clear it in finished restoring, the reason is the split can collapse before application:viewControllerWithRestorationIdentifierPath: is called, meaning it can't be retrieved via the window).

The reason we do not decode the object in the decodeRestorableStateWithCoder method is because that is called after viewDidLoad which is too late to set the things we require.

Submiss answered 17/7, 2018 at 14:50 Comment(0)
A
-2

I learned one very clean way of setting up the Core Data stack from NSScreencast. Basically, you start your Xcode project without choosing the "Use Core Data" option. Then you add a singleton class which is your data model. So to get the main MOC, you'd do [[DataModel sharedModel] mainContext]. I find that much cleaner that dumping everything in the App Delegate.

I've never used it this way, but I guess in your case you could also do this in your view controllers:

-(NSManagedObjectContext*)moc
{ 
    if (_moc != nil) return _moc;
    _moc = [[DataModel sharedModel] mainContext];
    return _moc;
}
Ammoniate answered 8/11, 2013 at 19:32 Comment(5)
The only thing missing from this is thread safety. If the sender of this message is not on the main thread, you should create an supply a context per thread, instead of sharing one context between threads.Prioress
Thanks, but this doesn't have anything to do with State Restoration. The question wasn't about how to set up a Managed Object Context, just how to make it work nicely with State Restoration.Goodard
FYI, this is trading one singleton for another. If you are going to use a singleton you might as well leave it in the App Delegate.Summersault
I've just never understood why Core Data stuff belongs in the app delegate at all. I like this singleton trade because I have OCD ;)Ammoniate
@Ammoniate using a singleton for CD is something that sounds like a good idea at first, but as your app gets more complicated you see why you don't want to do it. As you start spinning off other MOCs it becomes a bit of a pain.Goodard
M
-2

My solution to this has been to make the view controllers to default to use a global shared MOC. This context will be the intended one in most cases, and if you need to pass any other MOC it is perfectly possible to do so.

This approach confirms to Apple's "pass the baton"-approach as well as being both convenient and compatible with state restoration.

Milzie answered 30/4, 2014 at 9:43 Comment(1)
No, a global shared object would be the opposite of the pass the baton approach.Emblematize

© 2022 - 2024 — McMap. All rights reserved.