How do hit-testing and the responder chain work?
TL;DR
Hit-testing answers "which view was touched?". UIKit walks down the view tree and finds the deepest view under the finger.
The responder chain answers "who handles this event?". If a responder does not handle it, it passes it to its next responder, going up.
Hit-testing step by step
UIKit calls hitTest(_:with:) on the window. Each view:
- Ignores the touch if
isHidden,!isUserInteractionEnabled, oralpha < 0.01. - Checks
point(inside:with:). If the point is outside its bounds, it returnsnil. - Asks its subviews, last subview first (the one on top).
- Returns the first subview that returns a view. If none, returns itself.
This is roughly what the default does:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard isUserInteractionEnabled, !isHidden, alpha >= 0.01,
self.point(inside: point, with: event) else { return nil }
for subview in subviews.reversed() {
let converted = subview.convert(point, from: self)
if let hit = subview.hitTest(converted, with: event) {
return hit
}
}
return self
}Classic use: a bigger tap area
A 24 pt icon is hard to tap. Make the touch area bigger without changing the look.
final class BigTapButton: UIButton {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
bounds.insetBy(dx: -10, dy: -10).contains(point)
}
}Classic bug: a subview outside its parent
If a button sticks out of its superview, taps on the outside part do nothing.
The parent's point(inside:) returns false, so UIKit never asks the button.
clipsToBounds = false only changes drawing, not touches. Fix it by overriding hitTest in the parent.
The responder chain
UIResponder is the base class of UIView, UIViewController, UIWindow and UIApplication.
Each one has a next property.
UIView -> superview ... -> UIViewController's root view -> UIViewController
-> (parent view controllers) -> UIWindow -> UIApplication -> AppDelegateTouches, motion events, key presses and menu actions travel up this chain.
Sending actions with a nil target
You can send an action to "whoever handles it" up the chain.
// Dismiss the keyboard from anywhere:
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)UIKit starts at the first responder and walks up until some object implements the method.
This is how Edit menu actions like copy(_:) find the right text field.
First responder
The first responder gets keyboard input first. A text field becomes first responder when tapped.
textField.becomeFirstResponder() // show keyboard
textField.resignFirstResponder() // hide keyboard
view.endEditing(true) // hide keyboard for any subviewGesture recognizers
Gesture recognizers get touches before the hit view does.
If a tap gesture recognizes, the view's touches are cancelled by default (cancelsTouchesInView = true).
This is why a tap gesture on a parent can block taps on a button inside it.
Common follow-up questions
- How do you find a view's view controller? Walk
nextuntil you find aUIViewController. - Why does a button inside a transparent overlay not get taps? The overlay is on top and returns itself in
hitTest. SetisUserInteractionEnabled = falseon the overlay, or overridehitTestto returnnilfor itself. - Order of subviews in hit-testing? From top-most (last in
subviews) to bottom. - What does SwiftUI use?
contentShape(_:)changes the hit area.allowsHitTesting(false)turns it off.