Friday, September 7, 2012

Objective-C blocks for fun (part 2)

Last time I gave you a trick to run a block in a timer using a NSBlockOperation and I hope you enjoyed it ;)

Today, I will show you how to avoid delegate method implementations using blocks.


If you are like me, you love blocks because they let you tread code as data and thus allow you to use code here and there without the need to declare too much what your intent is to the compiler.  They are great for callback for instance. Although blocks are just a step forward of function pointers, they really make sense in todays development where one can easily go from C to Ruby or Javascript the same day and write lambdas or anonymous functions and blocks almost for the same kind of problem resolution.

But not all APIs are block aware. For example a lot of GUI classes in UIKit are still relying on the delegate paradigm although blocks would be a perfect fit for most of them. Hence, we create a lot of class wrappers ...

UIAlertView

Take for example the UIAlertView class. Most of the time you use it to either notify the user of something or to ask a Yes or No question like so:

UIAlertView *av = [[[UIAlertView alloc]initWithTitle:@"Alert" message:@"This is an alert" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:@"Ok", nil]autorelease];
[av show];

But if you need the user's feedback you need to set the delegate to an object (often self) and implement one of the optional methods of the UIAlertViewDelegate protocol.

I know, that's what we use to do before blocks, but now that we have blocks !?! Beuurk!

There are two ways of solving this problem. Either we create a class wrapper or a subclass and set the delegate to that new class and implement the protocol in that class .... Or, we could use the runtime to inject the missing method directly in the object, make itself it's own delegate, and call a block from there. WhaOooOoOo that's a heavy duty... Let's do that!

Background

There is a great explanation of the trampoline concept by Mike Ash here. Also, this solution is based of the comment of Keith Duncan, so thanks for the hint.

We will use the Objective-C runtime to inject method implementations, although it seems to works on iOS 4+ and MacOS 10.6+ I cannot guaranty that the AppStore approval process will not reject this kind of code. It does no swizzling, so it may pass the validations, please let me know if it doesn't.

So...Trampoline

Injecting a method implementation can be done at run time using class_addMethod(). This function takes 4 arguments:

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
  • a Class object
  • a Selector
  • an Implementation (a pointer to a function with a special signature)
  • a type encodings string
Ok, so if we were to inject a method implementation we would need a function BUT we would like to use a block, not a function. See, blocks are more or less functions that embed context. So basically it's a structure that not only includes a function with the code but also includes some state management etc...

So we need a function... too bad we couldn't access the block function at least in a portable manner.

Enters the trampoline.

The basic idea of the trampoline is to send you back when you hit it. In our case the trampoline would be a function. This function would be the method implementation and it will bounce the block back to the caller :)

Yay!

Now how do we do that?

Creating a method

Let's write the implementation function first. In our case, the UIAlertViewDelegate method we would like to inject is:

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex

It's return type is void, so we need a trampoline function that returns void. It also takes 2 arguments, one is of UIAlertView* kind and the other is a NSInteger, so the function should also have these two arguments.

An implementation in Objective-C is a type IMP which is a pointer to a function that takes 2 mandatory arguments:
  • id self
  • SEL _cmd
If the implementation requires more arguments they are put after the 2 mandatory arguments in the right order. So in our case the trampoline function signature should look like this:

void trampoline(id self, SEL _cmd, UIAlertView *alertView, NSUInteger buttonIndex);

Now, inside this function we need to call a block. Yes, that's the whole point of the article remember? :)
How can we do that? 

Fortunately, the runtime is here to help. There is a way to associate a value to an object. The association is done with this function:

void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)

Now, depending of the policy we use, the documentation says that the value will be released when the object gets deallocated, which mean that if we use any policy except  OBJC_ASSOCIATION_ASSIGN the object we provide will be released such that we don't need to hack the deallocation process. For our block to be persisted, we will force a heap copy with a policy of OBJC_ASSOCIATION_COPY_NONATOMIC (I think that if we restrain ourselves to the creation of blocks for UI objects delegation and thus stick on the main thread we can safely use the non atomic version, comments are welcome).

Next, we can get back the associated value whenever using: 

id objc_getAssociatedObject(id object, void *key)

So, here is the overall process:
  1. create a method implementation trampoline function that execute a block associated with self
  2. add the method to the UIAlertView instance
  3. set the UIAlertView delegate to itself
Create the implementation trampoline:


void trampoline(id self, SEL _cmd, UIAlertView *alertView, NSUInteger buttonIndex){
  void(^block)(UIAlertView*, NSInteger) = objc_getAssociatedObject(self, _cmd);
  if (block) block(alertView, buttonIndex);
}

That's the bounce back trampoline. Now Let's add the method to the UIAlertView *av we created above:

void(^block)(UIAlertView*, NSInteger) = ^(UIAlertView*alertView, NSInteger buttonIndex) {
  NSLog(@"Dismissed with button index: %d", buttonIndex);
};

objc_setAssociatedObject(av, @selector(alertView:didDismissWithButtonIndex:), block, OBJC_ASSOCIATION_COPY_NONATOMIC);

class_addMethod([av class], @selector(alertView:didDismissWithButtonIndex:), (IMP)trampoline, "v@:@i");

Then, set the delegate:

av.delegate = av;

And show the alertView:

[av show];

Conclusion

Although, we managed to inject the method and use a block as delegate, it is unclear to me if we should be doing that. First, we need to be sure the AppStore approval process will not reject an App using this kind of code. Then it may no be as portable as a wrapper or a subclass.

However, if this solution could be proven usable for small delegations like in this example, it is worth to note that it allows you, for example, to create a UIAlertView macro that you can reuse anywhere in your projects.

All in all, this is a simple example, and I managed to make a more global solution for this matter using preprocessor macros to create the trampoline function and a simple class to add a method to any object. The code can be found here on my github profile.

Cheers.

No comments:

Post a Comment