Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Easy way to do NSCoder enabled class

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.

like image 585
MapaX Avatar asked Jan 20 '12 07:01

MapaX


People also ask

What is a NSCoder?

NSCoder declares the interface used by concrete subclasses to transfer objects and other values between memory and some other format. This capability provides the basis for archiving (storing objects and data on disk) and distribution (copying objects and data items between different processes or threads).

What is NSCoding in Swift?

The NSCoding protocol declares the two methods that a class must implement so that instances of that class can be encoded and decoded. This capability provides the basis for archiving (where objects and other structures are stored on disk) and distribution (where objects are copied to different address spaces).


1 Answers

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.

like image 117
Nick Lockwood Avatar answered Sep 29 '22 18:09

Nick Lockwood