When you're writing an application, whatever platform you're writing it for and whatever framework you're writing it in, you're going to want your users to be able to customize some of it. If that isn't the case with you, you likely write cron jobs all day long and you might as well stop reading.
Those configurations that our users provide us with need to be stored somehow. Users don't like setting their application up each time they want to use it. Different platforms have different ways of approaching this problem; but generally speaking the approaches are very application-specific and terribly verbose in code. Code verbosity sucks. It makes your code complex and opaque. Your code should be simple and transparent.
So, what we all really want is a simple, short and generic way of gathering, storing and accessing user configuration; preferably in a way that involves strong type safety of the actual data that each configuration property reflects.
What I present to you today is an Objective C approach of doing just that.
Persistence
The first obstacle that we need to considder is the persistence of our configuration data. Collected user data is no good to us if we can't remember and recall it.
Apple provides us with a very useful utility here that goes by the name of NSUserDefaults. This is basically a utility class that allows you to specify keys for which values must be persisted and by which we can later recall them again. The NSUserDefaults takes care of persisting and loading these key and value pairs for us, so we needn't worry about any of that. As a matter of fact; the most we'll ever need to considder the actual persistence of our data is when we're setting an important value that we want to flush straight away (such that it's not reset to the old value should our application crash before NSUserDefaults decides it's time to flush its cache). In that particular case, we need to invoke synchronize on our NSUserDefaults object:
[self.defaults synchronize];
One caveat: Since we're dealing with persisting data here, we need to make sure that the data we store is actually persistable. In this particular case, that means it should be an instance of: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. In the case of collections, they may only contain elements that are instances of the same list of classes. Should you like to persist any other data; you'll need to archive it into an NSData object and unarchive it on access.
What about initial default values?
While persisting new configuration data is great and all, we still need to actually have default settings for most of our configuration properties before the user has touched them. These can't come from our persistence object because we've never persisted anything with it before; or can they?
NSUserDefaults, our weapon of choice regarding the storage and accessing of our configuration data, comes to the rescue. It provides a means of specifying default values for each of our configuration properties. The default value is returned by NSUserDefaults whenever we try to look something up that doesn't already have a user-persisted value.
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt: 24], dFontSize,
...,
nil]];
Accessing and modifying our settings
Great! We can store settings and recall them again later. Now if only we could actually access them from our application, that'd be awesome.
Let me ask you, what is the way for objects to provide other object access to their data? I'm sure you'll all agree with me that the answer to that is simply: Properties.
So that's what we'll use. Properties in Objective C are type-safe, they are short to declare and easy to access. We even get the choice between the [square bracket] and the dot.notation:
@property (readwrite, retain) NSNumber *fontSize;
That's it. Your application now knows how to access the fontSize configuration setting. It can look it up, and it can change it. What's more is that all this is nice and type-safe. Remember to always use the retain attribute on these properties. As a matter of fact; remember to use retain (or copy) on all your object properties unless you have a very specific and educated reason not to.
Now, while our application knows how to get to your settings; your configuration implementation doesn't yet know how to link up our NSUserDefaults with these properties. This step is a little bit more complex than what we've done so far.
Simply put
In essence, what we need to accomplish is invoking -setObject:forKey: on our NSUserDefaults instance with the key being the name of our configuration property and the value being whatever the user instructed us to store for that setting.
We could do it quite simply by implementing the setter for the property like thus:
- (void)setFontSize:(NSNumber *)fontSize {
[self.defaults setObject:fontSize forKey:dFontSize];
}
Our setter takes the value given to us by the application and stores it into our NSUserDefaults. All done. Accessing the setting is as easy as:
- (NSNumber *)fontSize {
return [self.defaults objectForKey:dFontSize];
}
The problem this introduces, however, is massive code verbosity. And as agreed earlier; code verbosity leads to opaqueness and is bad. Moreover, the amount of copy-pasting that would be required when implementing a host of configuration properties should give any decent software architect a splitting headache.
Each of these configuration property implementations will look almost identical. All they need to do is invoke either -setObject:forKey: or -objectForKey: on our NSUserDefaults instance. We should be able to extract all that duplicate code into one single method, right?
Clean that up
Of course right! This is Objective C; all is possible at runtime! The solution here is to introduce the concept of Message Forwarding.
The idea is to watch as our application invokes our property and intercept that invocation.
Instead of providing an implementation of our properties, we'll stop Objective C from trying to find such implementation and instead forward it to a single method that does all.
Forwarding relies on two things:
- Objective C needs to be able to create an NSInvocation object for your property's getter or setter.
- We need to intercept each call to our properties with missing implementation and handle them manually.
Since our methods are simple property getters and setters, they aren't all that complex to generate a method signature for. The signature comes from the way Objective C works internally. Any Objective C method call really just dereferences a C function pointer with a return value, the first argument being self and the second argument being _cmd which is the selector of the method that is being called.
Getters have a method signature that says: Return id, self is an id and _cmd is a SEL. That translates into an Objective C type array of: "@@:".
Setters have a method signature that says: Return void, self is an id, _cmd is a SEL and the first argument of my method is an id. This translates into an Objective C type array of: "v@:@".
Here's how we now generate the required method signatures from that information:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) hasPrefix:@"set"])
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
return [NSMethodSignature signatureWithObjCTypes:"@@:"];
}
That's done. On to number two: Intercepting the call to our missing property implementation.
Whenever Objective C is asked to perform an invocation on a selector that is not defined in the object instance you're calling it on, the runtime invokes the -forwardInvocation: method of that object instance to give it a chance to forward the invocation elsewhere. The default implementation of this method simply just calls -doesNotRecognizeSelector: which raises an exception. This is what causes our applications to terminate when we invoke selectors on objects that do not support them.
We're going to override this default behaviour and implement our own.
The goal is simple: Either set the object the application is calling our setter with in the NSUserDefaults or retrieve it for the getter the application is calling.
The only thing we still need to know before we can write the whole implementation for this is the name of the key in our NSUserDefaults that we should use to store our property's data. This key name should be something we can easily generate from the data we have when we're in that property's setter or getter. I decided to use the string representation of the property's getter. This gives me two nice perks:
- It's easy to generate while we're inside our -forwardInvocation: call.
- It provides an additional level of type-safety.
Type safety? How? Here's how: Whenever you need to reference the key name of a property outside of the -forwardInvocation: call (for instance, when setting defaults for that property!), use NSStringFromSelector(@selector(myProperty)) as the key for NSUserDefaults.
Now, provided you have -Wundeclared-selector set in your build settings' WARNING_CFLAGS (also called Other Warning Flags), you'll get the compiler complaining whenever you reference non-existing settings or miss-spell them. Great for knowing what to else to delete or change when you're removing or renaming a configuration property!
That aside, we now know what our key will be. So, let's get started with our forwarding implementation; and wrap this up!
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSString *selector = NSStringFromSelector(anInvocation.selector);
if ([selector hasPrefix:@"set"]) {
NSRange firstChar, rest;
firstChar.location = 3;
firstChar.length = 1;
rest.location = 4;
rest.length = selector.length - 5;
selector = [NSString stringWithFormat:@"%@%@",
[[selector substringWithRange:firstChar] lowercaseString],
[selector substringWithRange:rest]];
id value;
[anInvocation getArgument:&value atIndex:2];
[defaults setObject:value forKey:selector];
}
else {
id value = [defaults objectForKey:selector];
[anInvocation setReturnValue:&value];
}
}
It's really simple for when we're forwarding a getter. We just use the string representation of the invocation's selector and we have our key. For the setter we need to trim off the set prefix and lowercase the next character. That gives us the correct string representation.
Putting it all together
So, what we have now, is a single class that provides easy access to configuration properties and a dead-easy, type-safe way of adding new properties or refactoring (renaming/deleting) old properties.
Here's what this all looks like put together. Our tiny interface that defines all our configuration properties in beautiful simplicity:
@interface Config : NSObject {
NSUserDefaults *defaults;
}
@property (readwrite, retain) NSNumber *fontSize;
...
+ (Config *)get;
@end
Our slightly more complex but safe and (relatively) short implementation:
#define dFontSize NSStringFromSelector(@selector(fontSize))
@interface Config ()
@property (readwrite, retain) NSUserDefaults *defaults;
@end
@implementation Config
@synthesize defaults;
@dynamic fontSize, ...;
- (id)init {
if (!(self = [super init]))
return nil;
self.defaults = [NSUserDefaults standardUserDefaults];
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt: 24], dFontSize,
...,
nil]];
return self;
}
- (void)dealloc {
self.defaults = nil;
[super dealloc];
}
+ (Config *)get {
static Config *configInstance;
if(!configInstance)
configInstance = [[Config alloc] init];
return configInstance;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) hasPrefix:@"set"])
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
return [NSMethodSignature signatureWithObjCTypes:"@@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSString *selector = NSStringFromSelector(anInvocation.selector);
if ([selector hasPrefix:@"set"]) {
NSRange firstChar, rest;
firstChar.location = 3;
firstChar.length = 1;
rest.location = 4;
rest.length = selector.length - 5;
selector = [NSString stringWithFormat:@"%@%@",
[[selector substringWithRange:firstChar] lowercaseString],
[selector substringWithRange:rest]];
id value;
[anInvocation getArgument:&value atIndex:2];
[self.defaults setObject:value forKey:selector];
}
else {
id value = [self.defaults objectForKey:selector];
[anInvocation setReturnValue:&value];
}
}
@end


2 comments:
I've been trying to get into contact with you over a question I have involving the use of Terminal on a mac.
I realize this is lame and seemingly unprofessional but I am in need of assistance.
Don't know how often you check this so.. ya..
Feel free to just email me (remember to fix the To: address by replacing , by @ and appending .com - spambots).
Alternatively: my contact page.
Post a Comment