Skip to content
SwiftMind

The Responder Chain

How a touch or an action finds something willing to handle it.

IntermediateNot started7 min
On this page

When something can't handle an event, UIKit passes it to the next object up a chain of UIResponders. A view's next responder is usually its superview; the top view's is its view controller; a controller's is its parent or presenting controller, then the window, then the application, then the app delegate.

This is what makes nil-targeted actions work. sendAction(_:to: nil, ...) means "walk up from the first responder until someone implements this", which is how the system Cut/Copy/Paste menu reaches whichever text field is focused without anyone wiring it up.

Two separate mechanisms share the chain: hit testing decides *which view* a touch starts at, walking down the hierarchy; the responder chain decides *who handles it*, walking up.

A nil target is the responder chain doing the routing for you.
// Any responder can opt in by simply implementing the actionfinal class CardView: UIView {    override var canBecomeFirstResponder: Bool { true }
    @objc func markAsRead(_ sender: Any?) { … }
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {        action == #selector(markAsRead) ? true : super.canPerformAction(action, withSender: sender)    }}
// nil-target: "whoever in the chain handles this"UIApplication.shared.sendAction(#selector(CardView.markAsRead), to: nil, from: self, for: nil)
// Dismissing the keyboard is the same trick — resign whoever is first responderUIApplication.shared.sendAction(    #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
Show all 18 lines
Walking up the chain
UIButtonfirst responder
UIStackViewsuperview
Root view
UIViewControllerhandles it here
UIWindow
UIApplication → delegatelast stop

An event starts at a button, passes to its superview, then the view controller, then the window, application and app delegate until something handles it.

The event starts at the first responder.

Step 1 of 4
  1. The event starts at the first responder.
  2. Each responder that can't handle it passes to the next one up.
  3. The first responder that implements the action handles it, and the walk stops.
  4. If nobody does, it reaches the application and is silently discarded.

Under the hood

Getting to the *start* of the chain is a separate pass. hitTest(_:with:) walks the view hierarchy downward, asking each view point(inside:with:), and returns the deepest view containing the point. Views with isUserInteractionEnabled == false, isHidden, or alpha <= 0.01 are skipped entirely — which is why a view with alpha = 0 can't be tapped and one with alpha = 0.02 can.

A subview outside its parent's bounds is a common puzzle: it draws fine if the parent doesn't clip, but hit testing fails because the parent rejects the point before recursing. Overriding point(inside:with:) on the parent is the fix.

Note the chain isn't strictly the view hierarchy. A presented view controller's next responder is its *presenting* controller, and inputAccessoryView and UIMenuController splice themselves in too.

Quick check

A button sits partly outside its superview's bounds. The superview doesn't clip, so the button is visible. What happens when you tap the outside part?

  • It works normally
  • Nothing — hit testing stops at the superview, which reports the point isn't inside it
  • The tap goes to the view behind
  • It crashes
Reveal answer

Nothing — hit testing stops at the superview, which reports the point isn't inside it

hitTest asks the superview point(inside:with:) before recursing into its subviews. The point is outside the superview's bounds, so it returns nil and never considers the button — regardless of the fact that the button is drawn there, because drawing and hit testing are separate passes. Overriding point(inside:with:) on the superview to include the button's frame is the standard fix.

Interview question

Walk me through what happens between a finger touching the glass and your action method running.

Think first, then reveal

Two passes, in opposite directions.

First, hit testing, going down. UIKit calls hitTest(_:with:) on the window, which asks point(inside:with:) and then recurses into subviews back-to-front, returning the deepest view that contains the point. Views that are hidden, have user interaction disabled, or have alpha at or below 0.01 are skipped entirely. The result is the view the touch is delivered to.

Then the responder chain, going up. If that view doesn't handle the event, it goes to its next responder — normally the superview, then the view controller at the top of its hierarchy, then the parent or presenting controller, then the window, the application, and finally the app delegate. The first responder that implements the action handles it and the walk stops. If nobody does, the event is silently discarded.

For a target-action button with an explicit target, the walk is skipped — it goes straight there. The chain matters when the target is nil, which is how the system edit menu and the keyboard-dismissal trick work without knowing which field is focused.

The bug this explains most often is a button outside its parent's bounds: it's visible because the parent doesn't clip, but untappable because hit testing rejected the point at the parent before it ever looked at the button.

What an interviewer listens for

  • Separates the downward hit-test pass from the upward responder walk
  • Knows the skip conditions: hidden, interaction disabled, low alpha
  • Describes the chain beyond the view hierarchy — controller, window, app
  • Notes unhandled events are discarded silently
  • Applies it to a real bug

Key takeaway

Hit testing walks down to find where a touch lands; the responder chain walks up to find who handles it. Unhandled actions vanish silently, so debug by walking next.

Read these first

Related

0%
Quiz this concept