Cocoa AppKit Responder Chain

The whole responder chain is traversed like the following, via the documentation for NSApplication.sendAction(_:to:from:):

  1. Start with firstResponder in the key window
  2. Try every nextResponder in the chain
  3. Try the key window’s delegate
  4. Try the same for main window, if it’s different from the key window
  5. NSApplication tries to respond to the message itself
  6. NSApplication.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. NSViewControllers 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;
}