Utilize The Responder Chain For Target-Action
I don’t know about you, but I tend to forget that there is a responder chain in iOS. This post is to remind myself (and you) that the responder chain exists and that we can use it to react to button events.
The Responder Chain
In iOS, events (for example touch events) are delivered using the
responder chain. The responder chain consists of responder objects
(Apples words, not
mine).
If you have a look at the documentation, you may have noticed that UIView
and
UIViewController
are
responder objects. This means they inherit from UIResponder
:
When the user taps a view in the view hierarchy, iOS uses hit testing to figure out which responder object should get the touch event first. The process starts at the lowest level, the window. Then is propagates up the view hierarchy and checks for each view if the touch happened within its bounds. The last view in that process that got hit, receives the touch event first. If that view does not respond to the touch event, the event is passed to the next responder in the responder chain. Apple has a nice example how this works here. When a view tells iOS that it did not get hit, the subviews of that view aren’t checked.
This has an interesting consequence. When a button is outside of the
bounds of its superview but visible because clipsToBounds
of the
superview is set to false
, it does not
receive any touch events. So, when ever a button doesn’t work, remember
to check if it is in the bound of its superview.
Target-Action
The target-action mechanism can be set up that is also uses the
responder chain by setting the target to nil
. Then iOS asks
the first responder if it handles the action. If not the first responder
passes the action to the
nextResponder.
An Example
Here is an example. Let’s say that we have a view with a button and a
label as a subview of a view controllers view. We could set up the view
controller as the target for the button in viewDidLoad
like this
subview.button.addTarget(self, action: "onButtonTap:", forControlEvents: .TouchUpInside)
.
But we can also set the target to nil
and use the
responder chain. Here is the subview with the button and the label:
class ViewWithButtonAndLabel: UIView {
let button: UIButton
let label: UILabel
override init(frame: CGRect) {
label = UILabel()
label.textAlignment = .Center
label.text = "Touch the button"
button = UIButton(type: .System)
button.setTitle("The Button", forState: .Normal)
button.addTarget(nil, action: "onButtonTap:", forControlEvents: .TouchUpInside)
let stackView = UIStackView(arrangedSubviews: [label, button])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .Vertical
super.init(frame: frame)
backgroundColor = .yellowColor()
addSubview(stackView)
stackView.centerXAnchor.constraintEqualToAnchor(centerXAnchor).active = true
stackView.centerYAnchor.constraintEqualToAnchor(centerYAnchor).active = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The important line is highlighted. The target for action of the button
is set to nil
.
As described above, this means that the action propagates through the
responder chain util a responder object implements the action.
Here is how the view controller looks like:
class ViewController: UIViewController {
let viewWithButtonAndLabel = ViewWithButtonAndLabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .whiteColor()
view.addSubview(viewWithButtonAndLabel)
viewWithButtonAndLabel.translatesAutoresizingMaskIntoConstraints = false
let views = ["subView": viewWithButtonAndLabel]
var layoutConstraints = [NSLayoutConstraint]()
layoutConstraints += NSLayoutConstraint.constraintsWithVisualFormat("|-20-[subView]-20-|", options: [], metrics: nil, views: views)
layoutConstraints += NSLayoutConstraint.constraintsWithVisualFormat("V:|-20-[subView]-20-|", options: [], metrics: nil, views: views)
NSLayoutConstraint.activateConstraints(layoutConstraints)
}
func onButtonTap(sender: UIButton) {
viewWithButtonAndLabel.label.text = viewWithButtonAndLabel.label.text == "Yeah!" ? "Touch the button" : "Yeah!"
}
}
Even though, we do not set the target explicitly, the onButtonTap(_:)
method of the view controller gets called when the button is tapped
because it is the first responder object implementing a method with that
signature.
You can find the example code on github.
Conclusion
The responder chain is your friend. Try to understand it. Read the documentation. Use it to make your code more powerful.