Benedict's Soapbox

Using NSInvocation instead of performSelector:...

Objective-C is a dynamic language. It’s possible to use runtime reflection to inspect and call an objects methods. The most common way to do this is to use respondsToSelector: and the performSelector:… family of methods. The performSelector: methods are useful but they do suffer from one potential irritating restriction: you can only call methods with 0 or 1 arguments. A work around for this is to pack all the arguments into a dictionary (or array). This works, but it’s ugly because:

There is another way; NSInvocation. NSInvocation is a class that represents a method call. Cocoa uses it for it’s undo system and for message forwarding. Here’s an example that shows how to use NSInvocation instead of performSelector:.

EMKTextFormatter is an abstract base class. It converts an XML file into a NSAttributedString which can be displayed in an NSTextView. EMKTextFormatter is designed to be reusable for many different XML schemas. We use reflection and invocations to provide this flexibility.

@interface EMKTextFormatter : NSObject  
-(NSAttributedString *)formattedTextFromXMLFile:(NSURL *)xmlURL;

/*  
Formatting methods for elements must have selectors that match this naming scheme:  
apply{LOWERCASE_METHOD_NAME_WITH_LEADING_CAPITAL}:range:attribs:

Examples:  
-(BOOL)applyQuote:(NSMutableAttributedString *)text range:(NSRange)range attribs:(NSDictionary *)attribs;  
-(BOOL)applyQuoteattrib:(NSMutableAttributedString *)text range:(NSRange)range attribs:(NSDictionary *)attribs;  
-(BOOL)applyImagecaption:(NSMutableAttributedString *)text range:(NSRange)range attribs:(NSDictionary *)attribs;

Formatting methods should return YES if the formatting was successfully applied otherwise they should return NO.  
*/  
@end

@implementation EMKTextFormatter

//...

-(BOOL)applyFormattingToElement:(NSString *)elementName text:(NSMutableAttributedString *)text range:(NSRange)range attribs:(NSDictionary *)attribs  
{  
    //Create the selector for the method to call  
    NSString *elementNameHead = [elementName substringToIndex:1];  
    NSString *elementNameTail = [elementName substringFromIndex:1];  
    NSString *formatedElementName = [[elementNameHead uppercaseString] stringByAppendingString:[elementNameTail lowercaseString]];  
    NSString *selectorName = [NSString stringWithFormat:@"apply%@:range:attribs:", formatedElementName];  
    SEL formattingSelector = NSSelectorFromString(selectorName);
    
    if (![self respondsToSelector:formattingSelector])  
    {  
        NSLog(@"Cannot format element <%@>. %@ does not implement %@", elementName, [self class], selectorName);  
        return NO;  
    }

    //Create the method signature  
    //If there was an other method with an identical signature then instead of creating one we could use ???? to  
    //fetch the existing method signature.  
    NSString *typeString = [NSString stringWithFormat:@"%s%s%s%s%s%s", @encode(BOOL), //return value  
    @encode(id), //hidden 'self' argument  
    @encode(SEL), //hidden '_cmd' argument  
    @encode(NSMutableAttributedString), //first argument  
    @encode(NSRange), //second argument  
    @encode(NSDictionary *)]; //third argument  
    NSMethodSignature *methodSig = [NSMethodSignature signatureWithObjCTypes:[typeString UTF8String]];
    
    //Create the invocation  
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];  
    [invocation setSelector:formattingSelector];
    
    [invocation setArgument:&text atIndex:2];  
    [invocation setArgument:&range atIndex:3];  
    [invocation setArgument:&attribs atIndex:4];
    
    //Fire the invocation  
    [invocation invokeWithTarget:self];
    
    //Get the return value of the invocation  
    BOOL result = NO;  
    [invocation getReturnValue:&result];
    
    if (!result)  
    {  
        NSLog(@"Error applying formatting to element %@ at range %@, with attribs: %@", elementName, NSStringFromRange(range), attribs);  
    }
    
    return result;  
}
    
-(BOOL)applyQuote:(NSMutableAttributedString *)formatedText range:(NSRange)range attribs:(NSDictionary *)attribs  
{  
    BOOL result = NO;  
    //...  
    //Format the text here  
    //...  
    return result;  
}

@end  

It’s worth noting that we could call invokeWithTarget: using a performSelector:… method. Also we could improve performance by storing methodSig as this will be identical for each invocation.