Cocoa AppKit Responder Chain
The whole responder chain is traversed like the following, via the documentation for NSApplication.sendAction(_:to:from:)
:
- Start with
firstResponder
in the key window - Try every
nextResponder
in the chain - Try the key window’s delegate
- Try the same for main window, if it’s different from the key window
NSApplication
tries to respond to the message itselfNSApplication.delegate
is tried last
First, NSResponder.respondsToSelector(_:)
is called to check if the current candidate is a match; if this returns false, NSResponder.supplementalTarget(forAction:sender:)
is sent to get to another receiver to delegate the call to.
This way, responders can offload the real work to other objects. This is especially useful to route actions from e.g. NSViewController
s to actual service objects to do the work, and keep the view controllers smaller.
The app delegate (NSApplication.shared.delegate
) is used as a fallback when the current key window’s responder chain returns nil
. The NSApplicationDelegate
is not part of the responder chain, though. You can never reach it through iterating over nextResponder
!
This process is done for every main menu item that has no target
. It’s done for every target–action control, actually, including buttons and context menus.
Objective-C Reproduction
The GNUStep project’s reconstruction of the process can be found in -[NSApplication _targetForAction:window:]
:
- (id) _targetForAction: (SEL)aSelector window: (NSWindow *)window
{
id resp, delegate;
NSDocumentController *sdc;
if (window == nil)
{
return nil;
}
/* traverse the responder chain including the window's delegate */
resp = [window firstResponder];
while (resp != nil && resp != self)
{
if ([resp respondsToSelector: aSelector])
{
return resp;
}
if (resp == window)
{
delegate = [window delegate];
if ([delegate respondsToSelector: aSelector])
{
return delegate;
}
}
resp = [resp nextResponder];
}
/* in a document based app try the window's document */
sdc = [NSDocumentController sharedDocumentController];
if ([[sdc documentClassNames] count] > 0)
{
resp = [sdc documentForWindow: window];
if (resp != nil && [resp respondsToSelector: aSelector])
{
return resp;
}
}
/* nothing found */
return nil;
}