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]
- 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. - Provide a method to decode each property the same way.
This leads to two problems:
- Must manually match the @“keyName” to the @property name. The compiler does not help you here.
- 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