Easy way to do NSCoder enabled class

MapaX picture MapaX · Jan 20, 2012 · Viewed 7.1k times · Source

I have tons of objects which I want to save for offline use. Currently I use create NSCoder compliant classes for the objects and coded data to file to be available offline.

So in the .h I introduce the objects:

@interface MyClass : NSObject<NSCoding>{
NSNumber* myObject;}
@property(nonatomic,retain) NSNumber* myObject;

And in .m I make the inits:

- (id) initWithCoder: (NSCoder *)coder {
   if (self = [super init]) {
        [self setMyObject: [coder decodeObjectForKey:@"myObject"]];
   }
}

- (void) encodeWithCoder: (NSCoder *)coder { 
    [coder encodeObject: myObject forKey:@"myObject"];
}

So the class is just dummy storage with getter and setter. Is here any better way to do the decode / encode. Can I use somehow @dynamic or Key-value coding for encode and decode? Basically I want all the variables in class saved to file and back to object when program starts up. This approach work, but creating all classes takes time and effort.

Answer

Nick Lockwood picture Nick Lockwood · Jan 20, 2012

Yes, you can do this automatically. First import these into your class:

#import <objc/runtime.h> 
#import <objc/message.h>

Now add this method, which will use low-level methods to get the property names:

- (NSArray *)propertyKeys
{
    NSMutableArray *array = [NSMutableArray array];
    Class class = [self class];
    while (class != [NSObject class])
    {
        unsigned int propertyCount;
        objc_property_t *properties = class_copyPropertyList(class, &propertyCount);
        for (int i = 0; i < propertyCount; i++)
        {
            //get property
            objc_property_t property = properties[i];
            const char *propertyName = property_getName(property);
            NSString *key = [NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding];

            //check if read-only
            BOOL readonly = NO;
            const char *attributes = property_getAttributes(property);
            NSString *encoding = [NSString stringWithCString:attributes encoding:NSUTF8StringEncoding];
            if ([[encoding componentsSeparatedByString:@","] containsObject:@"R"])
            {
                readonly = YES;

                //see if there is a backing ivar with a KVC-compliant name
                NSRange iVarRange = [encoding rangeOfString:@",V"];
                if (iVarRange.location != NSNotFound)
                {
                    NSString *iVarName = [encoding substringFromIndex:iVarRange.location + 2];
                    if ([iVarName isEqualToString:key] ||
                        [iVarName isEqualToString:[@"_" stringByAppendingString:key]])
                    {
                        //setValue:forKey: will still work
                        readonly = NO;
                    }
                }
            }

            if (!readonly)
            {
                //exclude read-only properties
                [array addObject:key];
            }
        }
        free(properties);
        class = [class superclass];
    }
    return array;
}

Then here are your NSCoder methods:

- (id)initWithCoder:(NSCoder *)aDecoder
{
    if ((self = [self init]))
    {
        for (NSString *key in [self propertyKeys])
        {
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    for (NSString *key in [self propertyKeys])
    {
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
}

You have to be a bit careful with this. There are the following caveats:

  1. This will work for properties that are numbers, bools, objects, etc, but custom structs won't work. Also, if any of the properties in your class are objects that don't themeselves support NSCoding, this won't work.

  2. This will only work with synthesized properties, not ivars.

You could add error handling by checking the type of a value in encodeWithCoder before encoding it, or overriding the setValueForUndefinedKey method to handle a problem more gracefully.

UPDATE:

I've wrapped these methods up into a library: https://github.com/nicklockwood/AutoCoding - the library implements these methods as a category on NSObject so any class can be saved or loaded, and it also adds support for coding inherited properties, which my original answer doesn't handle.

UPDATE 2:

I've updated the answer to correctly deal with inherited and read-only properties.