Benedict's Soapbox About Me Selected Work

26 May 2012Using 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:

  • The dictionary must be packed in the calling methods
  • and unpacked in the receiving method(s)
  • You loose type information
  • It introduces a level of indirection thus making the code harder to read

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.


Make a Comment

:

:



Timeline