NSCoding, Without the Boilerplate

In Object Serialization With NSCoding we talked about how your can use the NSKeyedArchiver and the NSCoding protocol to easily save your custom model objects as Binary Plists for later retrieval. As a reminder, here is how we would implement NSCoding for a simple class “Foo”, with three properties:

@interface Foo : NSObject <NSCoding>
 
@property (nonatomic, assign) NSInteger property1;
@property (nonatomic, assign) BOOL property2;
@property (nonatomic, copy) NSString *property3;
 
@end
 
@implementation Foo
 
- (id)initWithCoder:(NSCoder *)coder
{
  if ((self = [super init]))
  {
    // Decode the property values by key, 
    // and assign them to the correct ivars
    _property1 = [coder decodeIntegerForKey:@"property1"];
    _property2 = [coder decodeBoolForKey:@"property2"];
    _property3 = [coder decodeObjectForKey:@"property3"];
  }
  return self;
}
 
- (void)encodeWithCoder:(NSCoder *)coder
{
  // Encode our ivars using string keys
  [coder encodeInteger:_property1 forKey:@"property1"];
  [coder encodeBool:_property2 forKey:@"property2"];
  [coder encodeObject:_property3 forKey:@"property3"];
}
 
@end

NSCoding eliminates a lot of the complexity of saving objects by automatically handling circular references, duplicate objects, etc. But you still have to write those pesky initWithCoder: and encodeWithCoder: methods for every class. That’s kinds of a drag. Implementing NSCoding can involve a lot of boilerplate, especially if your object has a lot of properties to encode. Is there anything we can do about that?

Work Smarter, Not Harder

Let’s look at what we actually need to do for each property that we’re encoding. For each property we’ve written the following:

 
// In the initWithCoder: method
self.someProperty = [coder decodeObjectForKey:@"someProperty"];
 
// In the encodeWithCoder: method
[coder encodeObject:self.someProperty forKey:@"someProperty"];

That’s four references to the name of our property. Two of the references are selectors, two of them are strings. Not very DRY. We need to find some way to eliminate this repetition.

We can make use of KVC (Key-Value Coding) to set and get properties by name. KVC also has the neat feature that it can automatically box and unbox primitive values such as integers or booleans into their equivalent object type (e.g. NSNumber), so we don’t have to worry about using different methods to encode our properties, we can treat them all as objects. Using KVC we can rewrite our encode/decode calls as follows:

NSString *const PropertyName = @"someProperty";
 
// In the initWithCoder: method
id value = [coder decodeObjectForKey:PropertyName];
[self setValue:value forKey:PropertyName];
 
// In the encodeWithCoder: method
id value = [self valueForKey:PropertyName];
[coder encodeObject:value forKey:PropertyName];

Because we’ve eliminated any references to the specific property name in our code, we can now make it reusable. By looping through an array of property names, we can encode/decode all our properties easily. That means we can create a common base class for all our codable objects that does most of the work for us:

@interface CodableObject : NSObject <NSCoding>
 
- (NSArray *)propertyNames;
 
@end
 
@implementation CodableObject
 
- (NSArray *)propertyNames
{
  // Override this in the subclass to return an array of property names
  return nil;
}
 
- (id)initWithCoder:(NSCoder *)aDecoder
{
  if ((self = [self init]))
  {
    // Loop through the properties
    for (NSString *key in [self propertyNames])
    {
      // Decode the property, and use the KVC setValueForKey: method to set it
      id value = [aDecoder decodeObjectForKey:key];
      [self setValue:value forKey:key];
      }
  }
  return self;
}
 
- (void)encodeWithCoder:(NSCoder *)aCoder
{
  // Loop through the properties
  for (NSString *key in [self propertyNames])
  {
    // Use the KVC valueForKey: method to get the property and then encode it
    id value = [self valueForKey:key];
    [aCoder encodeObject:value forKey:key];
  }
}
 
@end

To implement our NSCodable Foo class from earlier, all we have to do now is this:

@interface Foo : CodableObject
 
@property (nonatomic, assign) NSInteger property1;
@property (nonatomic, assign) BOOL property2;
@property (nonatomic, copy) NSString *property3;
 
@end
 
@implementation Foo
 
- (NSArray *)propertyNames
{
  return @[@"property1", @"property2", @"property3"];
}
 
@end

So much nicer! But if we can reduce our encoding down to an array of property names, can’t we just get Cocoa to figure that bit out for us too?

Introspection FTW!

The objective-C runtime can help us out here. Using a bit of runtime magic, we can find out the names of all the properties of our class and generate the propertyNames array automatically in our CodableObject class. Here’s the code to do that:

// Import the Objective-C runtime headers
#import <objc/runtime.h> 
 
- (NSArray *)propertyNames
{    
  // Get the list of properties
  unsigned int propertyCount;
  objc_property_t *properties = class_copyPropertyList([self class], 
    &propertyCount);
  NSMutableArray *array = [NSMutableArray arrayWithCapacity:propertyCount];
  for (int i = 0; i < propertyCount; i++)
  {
    // Get property name
    objc_property_t property = properties[i];
    const char *propertyName = property_getName(property);
    NSString *key = @(propertyName);
 
    // Add to array
    [array addObject:key];
  }
 
  // Remember to free the list because ARC doesn't do that for us
  free(properties);
 
  return array;
}

Now any object that inherits from CodableObject supports NSCoding automatically without needing to override the propertyNames method. Sweet! There are a few caveats though:

1) This mechanism won’t encode properties inherited from a superclass. CodableObject doesn’t have any properties, but if we had a deeper inheritance structure (e.g. Dog > Animal > CodableObject) then the properties inherited from the Animal class wouldn’t get coded when we save an object of class Dog.

2) Not every property can be NSCoded. If a property is both virtual (not backed by an ivar) and readonly, it can’t be set using setValueForKey:, which means it will crash our initWithCoder: method. If it’s readonly and has an ivar, but the ivar name doesn’t match the property name, setValueForKey: won’t work either. Encoding readwrite virtual properties will work, but it’s probably pointless – if they have no backing ivar, they are probably computed values and don’t need to be saved. To be safe, we really only want to encode properties that have a backing ivar with the same name.

3) Not every property will be codable. If it’s something like a struct or pointer, or an object which doesn’t itself support NSCoding, it won’t work.

4) Sometimes we may want to omit properties from coding. For example if our class contains something like a timer, or some flags used purely for tracking runtime state, we may not wish to save those.

To fix issue 1 we need to loop through the object’s superclasses and make sure we capture all the properties in those classes too. To fix issue 2 we need to check some extra metadata about each property. Our improved propertyNames method looks like this:

- (NSArray *)propertyNames
{
  // Loop through our superclasses until we hit NSObject
  NSMutableArray *array = [NSMutableArray array];
  Class subclass = [self class];
  while (subclass != [NSObject class])
  {
    unsigned int propertyCount;
    objc_property_t *properties = class_copyPropertyList(subclass,  
      &propertyCount);
    for (int i = 0; i < propertyCount; i++)
    {
      // Get property name
      objc_property_t property = properties[i];
      const char *propertyName = property_getName(property);
      NSString *key = @(propertyName);
 
      // Check if there is a backing ivar
      char *ivar = property_copyAttributeValue(property, "V");
      if (ivar)
      {
        // Check if ivar has KVC-compliant name
        NSString *ivarName = @(ivar);
        if ([ivarName isEqualToString:key] || 
          [ivarName isEqualToString:[@"_" stringByAppendingString:key]])
        {
            // setValue:forKey: will work
            [array addObject:key];
        }
        free(ivar);
      }
    }
    free(properties);
    subclass = [subclass superclass];
  }
   return array;
}

That solves issue 1 and 2 and should now work for all common cases. There isn’t any simple solution to 3, but it’s not a big deal because we can avoid adding properties to our classes that can’t be auto-coded in most cases, and when we can’t, we can just override the NSCoding methods and handle them manually as a special case.

What about issue 4? How can we omit properties from being coded? If the property is private, we can just declare an ivar and no property declaration. We only loop through properties, so ivars with no associated property won’t be encoded.

For public properties, we can always override our propertyNames array to omit properties we don’t want to save, but that’s a bit messy. Fortunately our solution to problem 2 gives us a simple way to exclude certain properties from coding. We exclude properties from the propertyNames array if their ivar doesn’t match the name of the property. We do this out of technical necessity, but we can take advantage of it here. If we add a synthesise statement that defines the property as having a non-compliant name, it won’t be encoded. E.g.

@synthesize foo = foo; // This *will* be encoded
@synthesize foo = _foo; // So will this
@synthesize foo = foo_; // But this *won't* be
@synthesize foo = bar; // Nor will this

One last thing: The propertyNames method is pretty fast, but it seems inefficient to call it every time we encode or decode a class since the property names are decided at compile time and aren’t likely to ever change during program execution. Is there some way we can cache the propertyNames array for each class (preferably without needing to add extra properties or methods to our subclasses)?

Cool By Association

In Adding Properties to a Category Using Associated Objects we talked about using associated objects to add extra data to existing objects. Are you thinking what I’m thinking? No? OK, well I’m thinking we should use the associated objects mechanism to store our propertyNames array in the class once it has been calculated for the first time. You can do that as follows:

- (NSArray *)propertyNames
{
  // Check for a cached value (we use _cmd as the cache key, 
  // which represents @selector(propertyNames))
  NSMutableArray *array = objc_getAssociatedObject([self class], _cmd);
  if (array)
  {
      return array;
  }
 
  // Loop through our superclasses until we hit NSObject
  array = [NSMutableArray array];
  Class subclass = [self class];
  while (subclass != [NSObject class])
  {
    unsigned int propertyCount;
    objc_property_t *properties = class_copyPropertyList(subclass, 
      &propertyCount);
    for (int i = 0; i < propertyCount; i++)
    {
      // Get property name
      objc_property_t property = properties[i];
      const char *propertyName = property_getName(property);
      NSString *key = @(propertyName);
 
      // Check if there is a backing ivar
      char *ivar = property_copyAttributeValue(property, "V");
      if (ivar)
      {
        // Check if ivar has KVC-compliant name
        NSString *ivarName = @(ivar);
        if ([ivarName isEqualToString:key] || 
          [ivarName isEqualToString:[@"_" stringByAppendingString:key]])
        {
            // setValue:forKey: will work
            [array addObject:key];
        }
        free(ivar);
      }
    }
    free(properties);
    subclass = [subclass superclass];
  }
 
  // Cache and return array
  objc_setAssociatedObject([self class], _cmd, array, 
    OBJC_ASSOCIATION_RETAIN_NONATOMIC);
   return array;
}

And there we have it. Automatic NSCoding for all your classes, without the boilerplate.

NSCoding Additional Reading


Nick Lockwood is the author of iOS Core Animation: Advanced Techniques. Nick also wrote iCarousel, iRate and other Mac and iOS open source projects.
  1. Nice work! I’ve been toying around with the Mantle framework, which is your post taken REALLY far. It’s nice to just see the basics here as sometimes Mantle becomes and unwieldy beast…

Comments are closed.