blog

Photo by Benh LIEU SONG, http://www.flickr.com/photos/blieusong/7234335792

Persisting View Controllers With Core Data Objects

by

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:

  1. 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.
  2. Write an NSManagedObject subclass that a project’s core data objects inherit from, and implement encodeObject:forKey: in that subclass by storing self.objectID.  However, it seems somewhat intrusive to force NSManagedObjects to use a subclass just for the purposes of encoding and decoding.
  3. 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:

  1. Encode managed object IDs as URIs during state preservation
  2. Decode the URIs and fetch the objects from a given managed object context during state restoration
  3. Store the encoded NSData in the coder object provided by encodeRestorableStateWithCoder:, and consume it in decodeRestorableStateWithCoder:

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:

+ more