It’s common for iOS applications to preserve their state when quitting, so that the next time the user launches the app, their previous session is restored. Apple reduced the development burden for this state restoration in iOS 6 with their State Preservation and Restoration APIs. Even with the APIs, though, each view controller is responsible for preserving its own state, and that includes any references to core data objects that are being displayed in a table or detail view.
Of course, the persistence of NSManagedObjects
is handled by the core data stack, so the view controllers just need to store references that will allow them to refetch the objects during restoration. What’s a straight-forward way to implement encoding those references?
Approaches to preserving [NSManagedObject objectID]
The objectID property of managed objects should be saved, as it provides a unique, persistent, encodable value that can be used to restore objects from a managed object context after restarting an app.
Apple recommends preserving the managed object IDs encoded as URIs. Quoting from the core data programming guide:
You can also transform an object ID into a URI representation:
NSURL *moURI = [[managedObject objectID] URIRepresentation];
Given a managed object ID or a URI, you can retrieve the corresponding managed object using managedObjectIDForURIRepresentation: or objectWithID:.
An advantage of the URI representation is that you can archive it—although in many cases you should not archive a temporary ID since this is obviously subject to change. You could, for example, store archived URIs in your application’s user defaults to save the last selected group of objects in a table view.
Consider three options for encoding the object ID URIs of a view controller’s NSManagedObjects during the encodeRestorableStateWithCoder:
stage:
- Add the object IDs directly to the coder, perhaps with a custom
encodeManagedObject:forKey:
method in an NSCoder category. However, this approach makes encoding arrays of managed objects cumbersome. - Write an NSManagedObject subclass that a project’s core data objects inherit from, and implement
encodeObject:forKey:
in that subclass by storingself.objectID
. However, it seems somewhat intrusive to force NSManagedObjects to use a subclass just for the purposes of encoding and decoding. - Write NSCoder subclasses that recognize and encode core data objects, and store the resulting data in the coder provided by the state restoration API.
Subclassing NSCoder to preserve [NSManagedObject objectID]
The essence of the third option is writing a pair of new NSCoder classes that:
- Encode managed object IDs as URIs during state preservation
- Decode the URIs and fetch the objects from a given managed object context during state restoration
- Store the encoded NSData in the coder object provided by
encodeRestorableStateWithCoder:
, and consume it indecodeRestorableStateWithCoder:
Here’s an example of calling these classes, using helper methods that hide away the creation of the concrete NSCoder instances. It demonstrates how few lines of code the view controller needs in order to persist and restore an array of core data references.
#pragma mark - State preservation
- (void)encodeRestorableStateWithCoder:(NSCoder*)coder
{
[super encodeRestorableStateWithCoder:coder];
// Preserve the managed objects
NSData* data =
[AManagedObjectArchiver ArchiveDataFromBlock:^(AManagedObjectArchiver* archiver)
{
[archiver encodeObject:self.rabbits forKey:@"rabbits"];
}];
[coder encodeObject:data forKey:@"rabbits"];
}
- (void)decodeRestorableStateWithCoder:(NSCoder*)coder
{
[super decodeRestorableStateWithCoder:coder];
// Restore the rabbits
NSMutableData* data = [coder decodeObjectForKey:@"rabbits"];
[AManagedObjectUnarchiver UnarchiveData:data
Context:[NSManagedObjectContext MR_defaultContext]
Block:^(AManagedObjectUnarchiver* unarchiver)
{
self.rabbits = [unarchiver decodeObjectForKey:@"rabbits"];
}];
}
And below is an NSURL wrapper class that allows the unarchiver to recognize when it needs to fetch an NSManagedObject based on a decoded NSURL, followed by the NSKeyedArchiver and NSKeyedUnarchiver subclasses.
@interface ACodableManagedObject : NSObject <NSCoding>
@property (nonatomic, strong) NSURL* url;
+ (id)ObjectWithManagedObject:(NSManagedObject*)obj;
@end
@implementation ACodableManagedObject
+ (id)ObjectWithManagedObject:(NSManagedObject*)obj
{
ACodableManagedObject* codableObj = nil;
if (nil != obj)
{
codableObj = [[ACodableManagedObject alloc] init];
codableObj.url = obj.objectID.URIRepresentation;
}
return codableObj;
}
- (id)initWithCoder:(NSCoder*)aDecoder
{
self = [super init];
if (nil != self)
{
self.url = [aDecoder decodeObjectForKey:@"url"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder*)aCoder
{
[aCoder encodeObject:self.url forKey:@"url"];
}
@end
@interface AManagedObjectArchiver : NSKeyedArchiver
+ (NSData*)ArchiveDataFromBlock:(void(^)(AManagedObjectArchiver* archiver))codingBlock;
@end
@implementation AManagedObjectArchiver
+ (NSData*)ArchiveDataFromBlock:(void(^)(AManagedObjectArchiver* archiver))codingBlock
{
NSMutableData* data = [NSMutableData data];
AManagedObjectArchiver* archiver =
[[AManagedObjectArchiver alloc] initForWritingWithMutableData:data];
codingBlock(archiver);
[archiver finishEncoding];
return data;
}
- (void)encodeObject:(id)obj forKey:(NSString*)key
{
if ([obj isKindOfClass:[NSManagedObject class]])
{
ACodableManagedObject* codableObj =
[ACodableManagedObject ObjectWithManagedObject:obj];
[self encodeObject:codableObj forKey:key];
}
else
{
[super encodeObject:obj forKey:key];
}
}
@end
@interface AManagedObjectUnarchiver : NSKeyedUnarchiver
+ (void)UnarchiveData:(NSData*)data
Context:(NSManagedObjectContext*)context
Block:(void(^)(AManagedObjectUnarchiver* unarchiver))decodingBlock;
@property (nonatomic, strong) NSManagedObjectContext* context;
@end
+ (void)UnarchiveData:(NSData*)data
Context:(NSManagedObjectContext*)context
Block:(void(^)(AManagedObjectUnarchiver* unarchiver))decodingBlock
{
AManagedObjectUnarchiver* unarchiver = [[AManagedObjectUnarchiver alloc] initForReadingWithData:data];
unarchiver.context = context;
decodingBlock(unarchiver);
[unarchiver finishDecoding];
}
- (id)decodeObjectForKey:(NSString*)key
{
id obj = [super decodeObjectForKey:key];
if ([obj isKindOfClass:[ACodableManagedObject class]])
{
ACodableManagedObject* codableObj = obj;
NSURL* url = codableObj.url;
NSManagedObjectID* objectId =
[self.context.persistentStoreCoordinator
managedObjectIDForURIRepresentation:url];
obj = [self.context existingObjectWithID:objectId error:NULL];
}
return obj;
}
@end
The helper methods ArchiveDataFromBlock:
and UnarchiveData:Context:Block:
handle the work of creating, preparing, and cleaning up instances of the coders, so that clients of the coders can just pass in a block that uses the coders.
The coders themselves do nothing more than override encoding and decoding objects to recognized NSManagedObjects and ACodableManagedObjects.
Core data stack setup
The core data stack must be initialized prior to restoring managed objects in view controllers (if it’s not initialized, the app will throw an exception during state restoration). The new iOS 6 application delegate method application:willFinishLaunchingWithOptions:
is an appropriate place to initialize the stack. A recent state restoration blog post provides handy sample code in commonFinishLaunchingWithOptions:
if iOS 5 support is still required.
Final words
Apple’s new APIs make restoring view controllers almost trivial (assuming a project uses storyboards and controller transitions exactly as Apple intended). Managing the custom state of those view controllers is still the responsibility of the programmer, but with the right classes, restoring core data objects should be easy.
Further reading:
- Apple’s sample State Restoration project
- Apple’s WWDC 2012 video on state restoration (requires developer login)