Mapping Strings to Selectors

By uliwitness

MappingStringsToSelectorsSketchBack in the old days of Carbon, when you wanted to handle a button press, you set up a command ID on your button, which was a simple integer, and then implemented a central command-handling function on your window that received the command ID and used a big switch statement to dispatch it to the right action.

In Cocoa, thanks to message sending and target/action, we don’t have this issue anymore. Each button knows the message to send and the object to send it to, and just triggers the action directly. No gigantic switch statement.

However, we still have a similar issue in key-value observing: When you call addObserver:forKeyPath:options:context, all key-value-observing notifications go through the one bottleneck: observeValueForKeyPath:ofObject:change:context:. So, to detect which property was changed, you have to chain several if statements together and check whether the key path is the one you registered for (and check the ‘context’ parameter so you’re sure this is not just a KVO notification your superclass or subclass requested), and then dispatch it to a method that actually reacts to it.

It would be much nicer if Apple just called a method that already contained the name of the key-value-path, wouldn’t it? E.g. if the key-path you are observing is passwordField.text, why doesn’t it call observerValueOfPasswordField_TextOfObject:change:context:?

But there is a common Cocoa coding pattern that can help us with this: Mapping strings to selectors. The centerpiece of this method is the NSSelectorFromString function. So imagine you just implemented observeValueForKeyPath:ofObject:change:context: like this:

-(void) observeValueForKeyPath: (NSString*)keyPath ofObject: (id)observedObject change: (NSDictionary*)changeInfo context: (void*)context
    NSString *sanitizedKeyPath = [keyPath stringByReplacingOccurrencesOfString: @"." withString: @"_"];
    NSString *selName = [NSString stringWithFormat: @"observeValueOf%@OfObject:change:context:", sanitizedKeyPath];
    SEL      action = NSSelectorFromString(selName);
    if( [self respondsToSelector: action] )
        NSInvocation * inv = [NSInvocation invocationWithMethodSignature: [self methodSignatureForSelector: action]];
        [inv setTarget: self]; // Argument 0
        [inv setSelector: action]; // Argument 1
        [inv setArgument: &observedObject atIndex: 2];
        [inv setArgument: &changeInfo atIndex: 3];
        [inv setArgument: &context atIndex: 4];
        [inv invoke]
        [super observeValueForKeyPath: keyPath ofObject: observedObject change: changeInfo context: context];

We build a string that includes the name of the key-path, turn it into an actual selector, and then use -performSelector:withObject:, or in more complex cases like this one NSInvocation, to actually call it on ourselves.

For cases that have no clear mapping like this, you can always maintain an NSMutableDictionary where the key is whatever string your input is and the value the selector name for your output, and then use that to translate between the two. When you make whatever call equivalent to addObserver: you have in that case, it would add an entry to the dictionary. That’s probably how NSNotificationCenter does it internally.

As Peter Hosey pointed out, another good use case for this pattern is -validateMenuItem: where one could turn the menu item’s action into a string and concatenate that with ‘validate’.

2 Comments Leave a comment

  1. Scott Morrison 2013-08-15 at 19:09 Reply

    Interesting idea

    Note that your selector as it stands will need 6 invocation arguments not 5 — according to the selector arg[2] should be the keyvalue itself, not the object

    eg observerValueOfPasswordField_Text:(arg[2]) ofObject:(arg[3]) change:(arg[4]) context:(arg[5])
    arg[0] being self
    arg[1] being the selector

    Still think KVO should be able to keep track of added observers so that it is easy to remove them all in one message

    [self removeObserversforObject:self];
    [super dealloc];

    And to be able to add observers for multiple key value in one fell swoop

    [self addObserver:self forKeypaths:@[path1,path2,path3] options: 0 context nil];

    Maybe I will get off me butt and write a category….

  2. Scott, oops, I left in an extra colon. Fixed.

Share your thoughts