iOS: Calling unknown methods from an unknown object

Posted on November 4, 2011. Filed under: Tutorials | Tags: , |

Introduction

What if you wanted to call an unknown method that belonged to an unknown object? Or even multiple unknown methods from multiple unknown objects?

Huh? What am I talking about? You don’t think you would want to do that? Well, guess what? I wanted to do that just this past week! Yep! I wanted to call an unknown method (or methods) of an unknown object (or bunch of unrelated objects).

What’s that? Why would I want to do that? Well, have you ever used a UIControl, such as a UIButton, and you wanted to make the click event trigger a particular method in one of your project class objects? How would you implement that? Well, it’s quite simple, you just do the following:


//create the button
UIButton *myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
//set it's trivial properties
[myButton setTitle:@"Click" forState:UIControlStateNormal];
[myButton setTitle:@"Clicked" forState:UIControlStateSelected];

//add method to be called when button is clicked
[myButton addTarget:self action:@selector(onClickReset) forControlEvents:UIControlEventTouchUpInside];

So, basically, I wanted to create my own custom addTarget:action:forCustomEvents: method that would execute a specified unknown method from a specified unknown target when my own custom Event was triggered in my custom Control.

Concept

To accomplish this I figured out that you can do this one of two ways:

  1. Using NSInvocation
  2. Using function pointer prototyping

The basic idea is that you create your event handling class that saves a reference to the target (or targets) and also a dictionary or array of selectors (a unique identifier to represent a method at runtime). This class then calls the selectors when the event is triggered. This happens without needing to know any details of the method or the target object, except for what is passed into the addTarget:action:forCustomEvents: method of he event handling class.

Implementation

The basic premise for both of the following implementations is that you don’t know anything about the target object. It is unknown to you. You really don’t know anything about the method to be executed, either. It is unknown to you. The only things is that you expect the method to have a certain format. For example, you may want to handle several types of methods. You may want to handle:

  1. A method with no return value and no arguments
  2. A method with no return value and a boolean argument
  3. A method with no return value and an NSString argument

In all honesty, the list could go on and on. It could include methods with more than one argument or methods with a return value, as well. However, I’m going to give you the implementations for what I list above. Anything else, you should be able to do yourself with what you learn here.

Basic Setup

Ok, the first thing we want to do is setup our custom event handling CustomUIControl class. My CustomUIControl class will only take one target, but will execute multiple methods based on multiple events. In this example I will use NSString keys as the event designators. You could easily implement a bitmask for your custom event constants, but I’ll just keep it simple for this one.

Here’s the basic code setup:

#import <UIKit/UIKit.h>

#define kCustomEventNoneKey		@"noneEventKey"
#define kCustomEventBOOLKey		@"booleanEventKey"
#define kCustomEventObjectKey		@"objectEventKey"

@interface CustomUIControl : UIControl
{
	//the target object that owns the methods to be executed
	id _handler;
	
	//mutable dictionary of selectors (SEL) and event keys
	NSMutableDictionary *_commands;
}

@property (nonatomic, assign) id handler;
@property (nonatomic, retain) NSMutableDictionary *commands;

- (void) assignHandler:(id)hanlder;
- (void) registerCommand:(SEL)selector forColorPickerEventKey:(NSString *)key;
- (void) clearAllCommands;
- (void) executeCommandWithKey:(NSString *)key;
- (void) executeCommandWithKey:(NSString *)key andParameter:(void *)arg;

@end

The header file code above should be self-explanatory. However, I just want to point out a few things. First of all, the custom event keys that you use can be anything at all, as long as each of them is unique, or different from the others. Notice also that I have specified two invocation methods (executeCommandWithKey:, and executeCommandWithKey:andParameter:) to handle the three scenarios mentioned above. This is because two of the three scenarios are only different because of the type of parameter that they accept. However, because I use (void *) as the parameter type for these invocation methods, it doesn’t matter what the parameter type is because (void *) can be used to represent any type.

Here’s the basic setup for the source file:

#import "CustomUIControl.h"

@implementation CustomUIControl

@synthesize handler = _handler;
@synthesize commands = _commands;

- (id)init
{
	if ((self = [super init]))
	{
		//create the mutable dictionary
		_commands = [[NSMutableDictionary alloc] initWithCapacity:4];
	}
	
	return self;
}

- (void)dealloc
{
	[_commands release];
	self.commands = nil;
	
	[super dealloc];
}

- (void) assignHandler:(id)handler
{
	if ([self.handler isEqual: handler] == NO)
	{
		[self setHandler:handler];
		[self clearAllCommands];
	}
}

- (void) registerCommand:(SEL)selector forColorPickerEventKey:(NSString *)key
{
	NSValue *commandObject = [NSValue valueWithPointer:selector];
	
	[self.commands removeObjectForKey:key];
	[self.commands setObject:commandObject forKey:key];	
}

- (void) clearAllCommands
{
	[self.commands removeAllObjects];
}

Just a few points to make about the source code above:

  1. Because this class only executes methods from one target or handler, if we change the handler, we clear out all the registered commands, because we don’t know if the new handler will use the same method signatures.
  2. When we register selectors in the commands dictionary, we have to store them as an object. NSValue is the best object container since it is designed to hold any scalar type, including pointers, structures and object ids.
  3. Also, any objects in the dictionary with the specified key is removed first in order to maintain only one method to be executed for one handler target for triggering one type of event.

1. Using NSInvocation

- (void) executeCommandWithKey:(NSString *)key
{
	//get the NSValue object from the dictionary
	NSValue *commandObject = [self.commands objectForKey:key];
	
	if (commandObject != nil)
	{
		//retrieve the selector data from the NSValue object
		SEL selector = [commandObject pointerValue];
		//get the method signature based on the selector and the handler's class
		NSMethodSignature *signature = [[self.handler class] instanceMethodSignatureForSelector:selector];
		//setup the invocation object based on the method signature
		NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
		
		//specify the method selector
		[invocation setSelector:selector];
		//invoke the handler's method
		[invocation invokeWithTarget:self.handler];
	}
}

- (void) executeCommandWithKey:(NSString *)key andParameter:(void *)arg
{
	NSValue *commandObject = [self.commands objectForKey:key];
	
	if (commandObject != nil)
	{
		SEL selector = [commandObject pointerValue];
		NSMethodSignature *signature = [[self.handler class] instanceMethodSignatureForSelector:selector];
		NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
		
		[invocation setSelector:selector];
		//index 2 refers to the first (and only) parameter
		[invocation setArgument:arg atIndex:2];
		[invocation invokeWithTarget:self.handler];
	}
}

I believe the code above should be self-explanatory. I just want to explain one thing. For the setArgument:atIndex: invocation method, the first two indices, 0 and 1, indicate the hidden arguments self and _cmd, respectively. They refer directly to the target (handler) and selector methods.

2. Using function pointer prototyping

- (void) executeCommandWithKey:(NSString *)key
{
	NSValue *commandObject = (NSValue *)[self.commands objectForKey:key];
	
	if (commandObject != nil)
	{
		//get the selector 
		SEL selector = [commandObject pointerValue];
		
		//specify the function pointer
		typedef void (*methodPtr)(id, SEL);
		
		//get the actual method
		methodPtr command = (methodPtr)[self.handler methodForSelector:selector];
		
		//run the method
		command(self.handler, selector);
	}
}

- (void) executeCommandWithKey:(NSString *)key andParameter:(void *)arg
{
	NSValue *commandObject = (NSValue *)[self.commands objectForKey:key];
	
	if (commandObject != nil)
	{
		//get the selector 
		SEL selector = [commandObject pointerValue];
		
		if ([key isEqualToString: kCustomEventBOOLKey])
		{
			//specify the function pointer
			typedef void (*methodPtr)(id, SEL, BOOL);
			
			//get the actual method
			methodPtr command = (methodPtr)[self.handler methodForSelector:selector];
			
			//run the method
			command(self.handler, selector);
		}
		else //kCustomEventObjectKey
		{
			//specify the function pointer
			typedef void (*methodPtr)(id, SEL, id);
			
			//get the actual method
			methodPtr command = (methodPtr)[self.handler methodForSelector:selector];
			
			//run the method
			command(self.handler, selector);
		}
	}
}

That’s it. I checked the key so that I use different function pointers because the argument types are different. However, the concept is basically the same.

3. Executing the commands


[myCustomUIControl executeCommandWithKey: kCustomEventNoneKey];

BOOL flag = YES;
[myCustomUIControl executeCommandWithKey: kCustomEventBOOLKey andParameter:&flag];

NSString *myString = @"something";
[myCustomUIControl executeCommandWithKey: kCustomEventObjectKey andParameter:&myString];

Conclusion

I personally like the use of NSInvocation, as it is cleaner, easier to follow and a bit more versatile if you have to handle multiple types of method signatures.

References

41,719 views
  • Featured Apps

  • Archive

  • Users Online

    Users: 1 Guest
  • Meta

  • Visitors

Liked it here?
Why not try sites on the blogroll...