Implementing -[UIApplication targetForAction:to:from:]

Updated: It turns out that UIKit likes to set the first responder to nil. At that point, my technique for finding the first responder winds up returning the UIApplication instance, which nullifies the technique. I’ve rolled back this change from our codebase; I’ll leave the blog post here, but I can’t advise anyone adopt this technique.

One of the best patterns that UIKit inherited from its older brother is the responder chain. It is a fantastic way to decouple UI controls from their targets and enables the same control to perform its function even as the user interface or controller layer changes around it.

On iOS, like on the Mac, UIApplication plays a central role in dispatching events to the responder chain: -[UIApplication sendAction:to:from:forEvent:] starts with the first responder and walks up the chain to find an object that can handle the provided action. But what if you just need to know whether such a responder exists, or you need to ask it further questions before you dispatch the action?

On the Mac, NSApplication has a method just for this purpose: -targetForAction:to:from: walks the responder chain in the exact same way as -sendAction:to:from:, but instead of dispatching the action, it simply returns the object that it would have dispatched to.

UIApplication is lacking this method, and its absence bit me and a coworker today. (UIApplication does inherit the -targetForAction:sender: method from UIResponder, but that just behaves normally: it walks up the responder chain starting at the application. The next stop and last stop on the chain is the app’s delegate.)

✻ ✻ ✻

To provide some context: our iOS applications have a subclass of UIBarButtonItem that represents an Undo button which can also be tapped-and-held to present Undo and Redo options. OUIUndoBarButtonItem listens for notifications from undo managers and enables or disables itself depending on whether there are actions that can be undone or redone. This is determined by responding YES to -canPerformAction:@selector(undo:/redo:) withSender:«the bar button item». But whom to ask?

Previously, OUIUndoBarButtonItem relied on a delegate and a separate target property to answer this question. This solution eschewed the responder chain entirely, which meant that the bar button item was inflexible to changes happening in the UI or controller. Yesterday, my coworker Jake and I were working inside OUIUndoBarButtonItem and decided to take advantage of the responder chain instead of rely on a statically-assigned target.

In this new scheme, the button should updated its enabledness based on whether anyone in the responder chain will respond to -undo: or -redo: But how do we start walking the responder chain if we have no target to start from? This is where you would use -[NSApplication targetForAction:to:from:] on the Mac. Barring that, you could also get the window’s firstResponder and manually walk the chain from there, but iOS doesn’t make that available either (and it wouldn’t make sense to ask for it from a bar button item anyway, since those aren’t views).

✻ ✻ ✻

But it turns out we have all the pieces necessary to rebuild -targetForAction:to:from ourselves. Since -[UIApplication sendAction:to:from:forEvent:] will start from the first responder if given a nil target, we can define our own action message that simply acts as a probe from which to find the search origin:

Important: Read the disclaimer at the top of this article!

static id _probedResponder;

@implementation UIApplication (TargetForAction)
- (id)_ks_targetForAction:(SEL)action to:(id)target from:(id)sender {
  if (target)
    return [target targetForAction:action withSender:sender];
  else {
    [self sendAction:@selector(_ks_probeForFirstResponder:) to:nil from:self forEvent:nil];
    id foundTarget = [_probedResponder targetForAction:action withSender:sender];
    _probedResponder = nil;
    return foundTarget;
}

@implementation NSObject (FirstResponderProbing)
- (void)_ks_probeForFirstResponder:(id)sender {
  _probedResponder = self;
}

I’ve broken with the Mac tradition a little here: -[NSApplication targetForAction:to:from:], when given a non-nil target, will simply return the target if it responds to the given selector, or nil otherwise. Here, I’m adopting iOS’s approach of starting at the provided target and working up the responder chain.

✻ ✻ ✻

So now OUIUnodBarButtonItem is able to enable and disable itself based on whether anyone in the responder chain can perform -undo: or -redo:, and Jake and I removed a whole bunch of fragile state management that had been spread among our controller layer. The responder chain is awesome.