Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Objective-C forwardInvocation:

I often do something like:

CoolViewController *coolViewController = [[CoolViewController alloc] init];
[self.navigationController pushViewController:coolViewController animated:YES];
[coolViewController release];

How would I, in a category of UINavigationController, override forwardInvocation: so that I could just instead do:

[self.navigationController pushCoolViewControllerAnimated:YES];
  1. Please include the relevant code in your answer, not just an explanation. Thank you!

  2. Feel free to comment on whether this is good practice. I'm also asking this for educational purposes, but it seems to me that in this case, the simplification in code may outweight the unnoticeable (correct?) cost in processing time & memory usage. Also, I come from a Ruby background and love to use dynamic programming to simplify things, e.g., dynamic finders (e.g., find_by_name) in Rails.

  3. Bonus points if you could implement pushCoolViewControllerAnimated:withBlock and invoke the block after initializing the view controller, allowing me to set certain instance variables on the view controller created.

UPDATE: I just remembered that ARC is coming soon. So this specific example may not be so helpful then, but still a great exercise/example that could be used in other cases, e.g., dynamic finders for Core Data & passing a block to configure the NSFetchRequest.

like image 323
ma11hew28 Avatar asked Dec 10 '22 07:12

ma11hew28


1 Answers

Use the dynamic method resolution mechanism described in the Objective-C Runtime Programming Guide, specifically, +[NSObject resolveInstanceMethod:]:

@implementation UINavigationController (FWD)
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSString *name = NSStringFromSelector(sel);
    NSString *prefix = @"push";
    NSString *suffix = @"Animated:";
    if ([name hasPrefix:prefix] && [name hasSuffix:suffix]) {
        NSRange classNameRange = {[prefix length],
            [name length] - [prefix length] - [suffix length]}
        NSString *className = [name substringWithRange:classNameRange];
        Class cls = NSClassFromString(className);
        if (cls) {
            IMP imp = imp_implementationWithBlock(
            ^(id me, BOOL animated) {
                id vc = [[cls alloc] init];
                [me pushViewController:vc animated:animated];
                [vc release];
            });
            class_addMethod(cls, sel, imp, "v@:c");
            return YES;
        }
    }
    return [super resolveInstanceMethod:sel];
}
@end

Of course, if UINavigationController already uses +resolveInstanceMethod:, you've now broken it. Doing this in a subclass of UINavigationController, or using method swizzling to enable invoking the original implementation, would solve that problem.

The version accepting a post-creation block is a straightforward extension (change the block parameters, change the type encoding, change the selector name pattern and how you extract the intended class name).

like image 65
Jeremy W. Sherman Avatar answered Dec 30 '22 11:12

Jeremy W. Sherman