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 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

  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 . I would love feedback.

 
5
Kudos
 
5
Kudos

Now read this

Faster build times by leveraging multi-core CPU!

How in the world is this not the default and how have I not heard of this before?!? Buried in this helpful blog post about making your XCode life easier, is this tip: Faster build times by leveraging multi-core CPU ? defaults write... Continue →