Wednesday, November 24, 2010

Making NSInvocation even more useful (and painless)

I recently discovered the usefulness of NSInvocation to decouple different parts of my code. Basically, one of my objects wants an action performed on it by another, but I don't always want to define an interface or expose one class to another.

NSInvocation wraps everything that's needed for a selector invocation: the target object, the selector, the arguments and even the result. However, there's a lot of boilerplate code required to set one up, which is one of the reasons it eluded me for so long:
SEL selector = @selector(mySelector:);
NSString *argument = @"Hello World!";
NSInvocation *invocation = [NSInvocation 
  invocationWithMethodSignature:
  [self methodSignatureForSelector:selector]];
[invocation setTarget:self];
[invocation setSelector:selector];
[invocation setArgument:&argument atIndex:2];
[invocation retainArguments];
Note that the first argument is at index 2, since self and _cmd are passed implicitly. I also make the invocation retain its argument to make it self-contained.

Now you can pass the invocation to any other object who only needs to invoke it:
[invocation invoke];
Having found a life-saving use for this construct, I set out to make life easier for myself:
@implementation NSInvocation (VikramsOneLineConstructors)
+ (NSInvocation *)invocationWithTarget:(id)target selector:(SEL)selector 
  arguments:(NSArray *)arguments {
  NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:
    [target methodSignatureForSelector:selector]];
  [invocation setTarget:target];
  [invocation setSelector:selector];
  for (NSInteger i = 0; i < [arguments count]; i++) {
    id argument = [arguments objectAtIndex:i];
    [invocation setArgument:&argument atIndex:2+i];
  }
  [invocation retainArguments];
  return invocation;
}
+ (NSInvocation *)invocationWithTarget:(id)target selector:(SEL)selector argument:(id)argument {
  return [NSInvocation invocationWithTarget:target selector:selector arguments:[NSArray arrayWithObject:argument]];
}
@end
I'm now a carefree convert to invocations, as I can simply do this:
button.action = [NSInvocation invocationWithTarget:self
  selector:@selector(browseURL:) argument:url];

Friday, April 16, 2010

Retrofitting iPad UISplitViewControllers

Here's a technique I developed while adding iPad support to our existing iPhone app. The view hierarchy is defined in a bog-standard iPhone XIB file, with a UITabBarController at its root. Each tab bar has a UINavigationController, which in turn holds a UIViewController subclass.

Creating a new XIB for the iPad and adding UISplitViewControllers to each tab was too much for my lazy bones, so I decided to do it programmatically at runtime, in my app delegate. Here's what the code looks like:
// Masterful retrofit hack: Wrap the view controllers inside split view controllers
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
  NSMutableArray *controllers = [NSMutableArray arrayWithCapacity:[tabBarController.viewControllers count]];

  for (UIViewController *controller in tabBarController.viewControllers) {
    UISplitViewController *split = [[[UISplitViewController alloc] init] autorelease];
split.tabBarItem = controller.tabBarItem;
    DetailViewController *detail = [[[DetailViewController alloc] init] autorelease]; // a detail view will come here
    UINavigationController *nav = [[[UINavigationController alloc] initWithRootViewController:detail] autorelease];
    split.viewControllers = [NSArray arrayWithObjects:controller, nav, nil];
    [controllers addObject:split];
  }

  tabBarController.viewControllers = controllers;
}
The code is iterating over the tab bar's controllers and inserting a split view into the mix.

At the other point in our code (where we would normally push views onto the navigation controllers), we detect the presence of a split view and do the appropriate thing:
if ([self.navigationController.parentViewController isKindOfClass:[UISplitViewController class]]) {
// iPad split view controller support
UISplitViewController *split = (UISplitViewController *)self.navigationController.parentViewController;
  UINavigationController *nav = [split.viewControllers objectAtIndex:1];
  DetailViewController *detail = [nav.viewControllers objectAtIndex:0];
  [detail initWithModel:phoneBookModel entryAtIndex:indexPath.row];
} else {
  UIViewController *controller = [[DetailViewController alloc] initWithModel:phoneBookModel entryAtIndex:indexPath.row];
  [self.navigationController pushViewController:controller animated:YES];
  [controller release];
}