View Controller Actor Chain

The design of UIViewController actor chain is an embodiment of iOS 8's adaptivity philosophy. It is built on top of view hierarchy. Don't confuse it with responder chain.

Most of the time you don't access actor chain directly but use the two action methods that are implemented with it:

  • -showViewController:sender:
  • -showDetailViewController:sender:

These two methods are used to adaptively show another view controller(shown view controller) from a view controller(showing view controller).

When they are called only the shown view controller is determined by the caller. The showing view controller may be the method call receiver or may not — it is determined by the actor chain. How the view is to be shown is also yet to be determined, by the showing view controller.

These two action methods figure out the showing view controller by calling -targetViewControllerForAction:sender:. It traverses the actor chain up from the receiver and queries every view controller along the way until it finds the target — the showing view controller.

The interesting part is how the target view controller is found, i.e., how -targetViewControllerForAction:sender: is implemented.

Surely, it repeatedly traces parent view controller to traverse up the view hierarchy.

From the hint of -targetViewControllerForAction:sender:'s documentation it is easy to guess it calls -canPerformAction:withSender:. But it is not enough. -canPerformAction:withSender: just calls -respondsToSelector:. If we use only it for query the first view controller that is tested, the receiver, would be immediately picked as the target view controller, which makes the whole chain meaningless.

We also need to test whether a view controller has a meaningful implementation of the action method, i.e., whether its class or any of its superclass overrides the action method. The test can be done like this:

[viewController methodForSelector:action] != [UIViewController instanceMethodForSelector:action]

What UIKit uses is a private method called +doesOverrideViewControllerMethod:inBaseClass: which calls class_getMethodImplementation. The end result should be the same.

Put it together, here is the implementation of -targetViewControllerForAction:sender::

- (UIViewController *)targetViewControllerForAction:(SEL)action sender:(id)sender {
    UIViewController *viewController = self;
    while (viewController) {
        if ([viewController canPerformAction:action withSender:sender] &&
            [viewController methodForSelector:action] != [UIViewController instanceMethodForSelector:action]) {
            return viewController;
        }

        viewController = viewController.parentViewController;
    }

    return viewController;
}

Two things to notice:

  1. It may return nil.
  2. It never returns self if self's class does not override the action.

Prepared with these knowledge, we can now implement -showViewController:sender::

- (void)showViewController:(UIViewController *)vc sender:(id)sender {
    UIViewController *target = [self targetViewControllerForAction:_cmd sender:sender];
    if (target) {
        return [target showViewController:vc sender:sender];
    } else {
        [self presentViewController:vc animated:YES completion:nil];
    }
}

-showDetailViewController:sender: would be very similar.

From the implementation above you can see:

  1. If target is nil action is performed using the default behavior.
  2. If target is self — which should never happen as explained before — it would cause infinite recursion.

In fact, it is the code pattern that we should use in our own action methods. Yes, we can define new action methods for view controllers and sometimes it is very useful. As an example, see Apple's AdaptivePhotos sample code. Let's review one of its custom action methods here:

- (BOOL)aapl_willShowingViewControllerPushWithSender:(id)sender
{
    // Find and ask the right view controller about showing
    UIViewController *target = [self targetViewControllerForAction:@selector(aapl_willShowingViewControllerPushWithSender:) sender:sender];
    if (target) {
        return [target aapl_willShowingViewControllerPushWithSender:sender];
    } else {
        // Or if we can't find one, we won't be pushing
        return NO;
    }
}

As you can see, it is indeed the same code pattern.