A Possible NSUserDefaults Alternative

It is often the case that we need to persist a few bits of info or at least a single object, like a “user”. Core data is too big for this, NSUserDefaults is sometimes not the best practice use case, and does not provide compiler auto-completion. The correct way is to create a (singleton) object for the data, and use [NSKeyedArchiver][appleDoc] for persistence, which is not super fast, but is straightforward and speed does not matter for small classes like this. It is not hard to use, but there are a few things that you have to do correctly. For a quick refresher, there are lots of examples, but this one is [fine][exampleTedious]

  1. provide a method to encode each property of the object by encodeForKey by giving it an NSString key that is the same name as the property.
  2. Provide a method to decode each property the same way.

This leads to two problems:

  1. Must manually match the @“keyName” to the @property name. The compiler does not help you here.
  2. Must remember to add/modify the @“keyName” every time you add/modify a property name that you want to persist.

The most tedious (and treacherous) way:

    [coder encodeObject:name forKey:@"name"];
    [coder encodeObject:subgroups forKey:@"jobs"];
    [coder encodeObject:tasks forKey:@"location"];

The less tedious way, using an array of keys and valueForKey:


- (NSArray *)keysForEncoding {
    return [NSArray arrayWithObjects:@"name", @"jobs", @"location", nil];
}
- (void)encodeWithCoder:(NSCoder *)aCoder {

    for (NSString *key in self.keysArray) {
        [aCoder encodeObject:[self valueForKey:key] forKey:key]; 
    }
}

The worst thing is that both still have boilerplate which is not compiler checked.

So, thinking about this, I was trying to come up with a way to use introspection to eliminate the manual entry of a key array. This led me to a Objective-C runtime method that fit the bill.

+ (NSArray *)keysFromProperties {

    u_int count;

    objc_property_t* properties = class_copyPropertyList([self class], &count);
    NSMutableArray* propertyArray = [NSMutableArray arrayWithCapacity:count];

    for (int i = 0; i < count ; i++) {
        [propertyArray addObject:@(property_getName(properties[i]))];
    }
    free(properties);

    return [NSArray arrayWithArray:propertyArray];
}

So now my encoding does not depend on any array that I must manually fill, it is just a method and looks like this:

- (void)encodeWithCoder:(NSCoder *)aCoder {

    for (NSString *key in [[self class] keysFromProperties]) {
        [aCoder encodeObject:[self valueForKey:key] forKey:key]; 
    }
}

So I can simply add object and primitive data properties directly in the @interface, and the class takes care of the rest. I get autocompletion in the compiler, so there are no strings to keep track of.

I have posted the code on [Github][gitHubLink] . I would love feedback.

[appleDoc]: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSKeyedArchiver_Class/Reference/Reference.html
[exampleTedious]: http://beensoft.blogspot.com/2011/09/xcode-snippet-2-archiving-objects-with.html

[gitHubLink]: https://github.com/chrisbrandow/autoKeyedPersistentSingleton

 
6
Kudos
 
6
Kudos

Now read this

On Learning Programming

Brian Kernighan and Dennis Ritchie in their classic The C Programming Language wrote that the biggest hurdle in starting beginning learning a programming language is to write a program to print the ‘Hello World’ on the screen, the rest... Continue →