Easy way to do NSCoder enabled class
Asked Answered
S

2

6

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.

Sunwise answered 20/1, 2012 at 7:21 Comment(0)
A
43

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.

Angelikaangelina answered 20/1, 2012 at 7:31 Comment(7)
Thanks for great answer, this is perfect for my purpose. I would give you a vote, but I don't have enough reputation..Sunwise
The code have small memory leak. You need to add: free(properties); to propertyKeys method.Sunwise
excellent answer. Just stumbled here by accident but I learned a lot just reading it.Sulcate
The one caveat I found is that this approach can not unarchive the inherited property from the base class. i.e, there is a property "uuid" in the base class, it can not be handled correctly in the derived class.Topology
Yes that's true. Check out the more advanced implementation in my AutoCoding library instead, it correctly handles inherited properties: github.com/nicklockwood/AutoCodingAngelikaangelina
I've updated the answer to use the implementation from AutoCoding.Angelikaangelina
Is there is a way to a encode and decode ivars also? I desperately need a solution to NSCode ivars automatically.Socket
R
0

The library https://github.com/nicklockwood/AutoCoding 's performance is not good since it does reflection using class_copyPropertyList everytime, and it seems it doesn't support some structs.

Check https://github.com/flexme/DYCoding, it should be as fast as the pre-compiled code, and it supports various property types.

Retorsion answered 12/7, 2016 at 4:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.