Paulo Andrade

Keeper of Secrets.

twitter github stackoverflow linkedin email
Late Responders — Sidestepping The Responder Chain
Mar 13, 2019
7 minutes read

The responder chain is a simple but powerful concept present in any Cocoa/Cocoa Touch app. Simply put, the responder chain is a linked list of responders (most commonly views and view controllers) to which an event or action is applied. When a responder doesn’t handle a particular message, it simply passes the message up the chain.

But this post isn’t about how the responder chain works, the documentation already does an excellent job at that. And there’s also this thorough explanation by Bruno Rocha mentioned on last week’s iOS Dev Weekly issue.

This post is about how the simplicity of the responder chain can work against you.

The problem

Imagine you have the typical master-detail application. On the left there a list of items and on the right you show the details of the selected item. Example applications are Mail, Contacts and Secrets. In such apps, this is how the responder chain could look like.

A simplified diagram of the responder chain on a master-detail application.

A simplified diagram of the responder chain on a master-detail application.

The List Controller handles the list of items and selection, the Detail Controller handles displaying a selected item and performing actions on it. Fairly simple right?

The problem arises when you want to handle an action at all times but the controller responsible for that action isn’t in the current responder chain starting at the first responder. Lets look at some examples to make this clear.

Keyboard shortcuts

Say you want to handle the up and down arrow keys to move the current selection. The List Controller is the one responsible for handling the selection so it makes sense it should be the one handling these events. On the Mac you would override -keyDown: and on iOS you would override -keyCommands and whenever the first responder is either the List Controller, the List View or any of its subviews this would work just fine.

But anytime the first responder is elsewere, such as on one of the Detail View’s subviews your shortcut stops working.

There are a few different and “hacky” solutions here:

  1. Move selection handling up the chain to Split Controller for example.

    This would work, but it’s not really the responsibility of the Split Controller to do this. This would also mean a tighter coupling of the Split Controller and the List Controller making controller reuse more difficult. And if you apply this solution every time you encounter this problem, soon the Split Controller will be 90% of you controller code.

  2. Also handle the arrow keys on the detail controller and notify.

    Arguably a better solution than the last one but still not ideal. By using notifications you avoid the coupling of the two view controllers. But if you have many different Detail Controller classes you must make sure all of them do this. And if you need to use the framework’s QuickLook view controller or a third party view controller you’ll need to subclass it.

  3. Modify the responder chain.

    You could also have the -nextResponder of the Detail Controller point to the List Controller instead of directly to the Split View. To me, this chain makes more sense than the one shown above and I’ve used this solution successfully on the Mac before. But it’s still a bit difficult to maintain. And on iOS it gets worse because there’s no setter for the next responder… meaning you’ll have to implement the setter on any Detail Controller class/subclass.

Although not suitable for the problem mentioned above, if you’re reading this because you’re implementing some keyboard shortcut on the Mac you should always consider using an NSMenuItem or setting the key equivalent on the corresponding NSControl if there is one. The reason why both of these solutions work is left as an exercise for the reader.

User activities & the Touch Bar

Handoff and Siri Shortcuts are implemented using the NSUserActivity class and the responder chain. The way they work is the current “user activity” is determined by whichever responder is the first to return a non-nil NSUserActivity from -userActivity.

So the Detail Controller could implement -userActivity and return a “Viewing item XYZ” NSUserActivity to inform the OS of the current user activity. But what if the first responder is on the List Controller side? The Detail Controller is not currently on the responder chain path but isn’t the user still viewing the item XYZ?

Similar to user activities, the controls that appear on the Touch Bar are decided by traversing the responder chain and composing the result of calling -touchBar on all responders to form the final layout.

If your Detail Controller returns an Edit button on its -touchBar it would also be nice this button was present when the user is interacting with the List Controller.

Again you could try to apply some of the “hacky” solutions mentioned above but the same drawbacks would apply.

Late Responders

To summarize what was said above, some features that rely on the responder chain should be available even when the relevant responder isn’t on the the chain’s path.

With that in mind I propose a solution I’m calling “Late Responders”. The way it works is that you setup a late responder registry further up the chain, such as the window. Then, the responders further down the chain can register a late responder with the registry and let it handle any features that should be always available.

This way even if the responder itself isn’t in the chain’s path, its late responder surely will be.

Lets look at the registry first. It’s simply responsible for maintaining a list of responders — it’s internal responder chain.

@interface OCLateResponderRegistry : NSObject

@property (nonatomic, strong, readonly) OCResponder *initialResponder;
@property (nonatomic, strong, readonly) OCResponder *lastResponder;

- (void)registerLateResponder:(OCLateResponder *)responder;
- (void)deregisterLateResponder:(OCLateResponder *)responder;

@end

The initialResponder and the lastResponder are the entry and exit points of the internal chain. Clients use the -registerLateResponder: and -deregisterLateResponder: to add and remove late responders.

The late responder class is also very simple and is meant to be subclassed.

@interface OCLateResponder : OCResponder
#if TARGET_OS_OSX
<NSTouchBarProvider>
#endif

- (instancetype)initWithWeight:(NSInteger)weight NS_DESIGNATED_INITIALIZER;
- (void)deregister;

@property (nonatomic, readonly) NSInteger weight;
@property (nonatomic, nullable, weak) OCLateResponderRegistry *registry;

#if TARGET_OS_OSX
@property (nullable, strong) NSTouchBar *touchBar NS_AVAILABLE_MAC(10.12.2);
#elif TARGET_OS_IOS
@property (nullable, strong) NSArray<UIKeyCommand *> *keyCommands;
#endif
@end

Support for setting a Touch Bar (macOS) and key commands (iOS) is already there. The weight property allows responders to be sorted on the registry’s internal chain but this is rarely necessary.

Finally, we just need a protocol to be implemented by the responder in the chain that will contain the registry.

@protocol OCLateResponderRegistering <NSObject>
@property (nonatomic, strong, readonly) OCLateResponderRegistry *lateResponderRegistry;
@end

Putting it all together

Lets look at the handling of the up and down arrow to change the list selection mentioned above. Your List Controller class could add the following late responder subclass to the registry.

@interface ListControllerLateResponder: OCLateResponder
@property (nonatomic, weak) ListController *listController;
- (void)moveUp:(id)sender;
- (void)moveDown:(id)sender;
@end

The implementation of -moveUp: and -moveDown: would simply relay the action to the listController.

The only thing missing is the registering that late responder. On the List Controller you would do something like this:

- (void)registerLateResponders
{
    OCLateResponderRegistry *registry = /* search the chain for the registry */;
    
    ListControllerLateResponder *lateResponder = [[ListControllerLateResponder alloc] init];
    lateResponder.listController = self;
    lateResponder.keyCommands = @[
        [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:0 action:@selector(moveUp:)],
        [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:0 action:@selector(moveDown:)],
    ];
    [registry registerLateResponder:lateResponder];
    self.lateResponder = lateResponder;
}

- (void)deregisterLateResponders
{
    [self.lateResponder deregister];
    self.lateResponder = nil;
}

You would then call the above methods on -viewWillAppear: and -viewDidDisappear: for example. By doing so, when the List Controller isn’t in the responder chain’s path, the late responder would pick up the up and down key events and the notify the List Controller to change the selection.

Making it easier with a proxy

Putting it all together was fairly easy, but it still involved creating a LateResponder subclass which can be quite tedious. Specially if you consider it’s only forwarding the -moveUp: and -moveDown: actions to the List Controller.

We can make this even easier by using a general purpose late responder proxy.

@interface OCLateResponderProxy : OCLateResponder

+ (instancetype)proxyForResponder:(InterfaceKitResponder *)responder;

- (instancetype)initWithProxiedResponder:(InterfaceKitResponder *)responder;
- (instancetype)initWithProxiedResponder:(InterfaceKitResponder *)responder weight:(NSInteger)weight NS_DESIGNATED_INITIALIZER; 

/** If this property is set, then only the specified selectors are proxied. */
@property (nullable, nonatomic, strong) NSArray<NSString *> *proxiedSelectorNames;

@end

By using this late responder proxy we can rewrite the - registerLateResponders method above and do away with the ListControllerLateResponder class.

- (void)registerLateResponders
{
    OCLateResponderRegistry *registry = /* search the chain for the registry */;
    
    OCLateResponderProxy *lateResponder = [[ListControllerLateResponder proxyForResponder:self];
    lateResponder.keyCommands = @[
        [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:0 action:@selector(moveUp:)],
        [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:0 action:@selector(moveDown:)],
    ];
    lateResponder.proxiedSelectorNames = @[
        NSStringFromSelector(@selector(moveDown:)),
        NSStringFromSelector(@selector(moveUp:))
    ];
    [registry registerLateResponder:lateResponder];
    self.lateResponder = lateResponder;
}

And that’s it. You can find the code for Late Responders here. As usual, pull requests and issues are welcome. You can also reply to this tweet.



Back to posts