The Wonderful Cocoa (Touch) Responder Chain
Ask an experienced Cocoa developer about the responder chain and I bet they’ll sing its praises. However, if you ask an iOS developer about it you might be greeted with a confused shrug. I myself have had sparing opportunities to use it. So let’s ask a first question: what’s it for?
My take of things is that the responder chain is a good way for disparate objects to talk to one another. Think of a view deep in the hierarchy wanting to send some message to a superview. My particular use case has a table view cell needing to talk with the view controller containing the table view itself. There are a few ways we can do this without the responder chain. A non-exhaustive list includes: callback closures passed down (very in-style for modern iOS development), using a delegate, or an old-fashioned Notification
(née NSNotification
).
So we know the problem we’re trying to solve. How does the responder chain work? The main method we’ll be working with is on UIApplication
.
func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool
Before we dive in, there’s another post worth your reading on the Black Pixel blog from a few years ago. I’ll be rehashing some of their post here, and hopefully adding some helpful details as well.
Let’s start with a code sample so we can be on the same page. You should be able to paste this into a brand new project and have it work (assuming Xcode 10 and Swift 4.2 here).
extension Selector {
static let trigger = #selector(ViewController.somethingHappenedForAResponder(sender:forEvent:))
}
final class HandledEvent: UIEvent {
let title: String
init(title: String) {
self.title = title
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Press Me", for: .normal)
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
view.addSubview(button)
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
@objc
func somethingHappenedForAResponder(sender: AnyObject?, forEvent event: HandledEvent) {
print("triggered event named \(event.title)")
}
@objc
func buttonPressed() {
print("button pressed")
let event = HandledEvent(title: "Foo")
UIApplication.shared.sendAction(.trigger, to: nil, from: self, for: event)
}
}
Most of this code is boilerplate to add the button and handle its touch. The chain of events is button press -> responder chain -> view controller triggered. Let’s look at it piece by piece.
- The button is tapped, calling
buttonPressed()
on our view controller
- The view controller creates a new instance of
HandledEvent
and calls the responder chain. This is triggered by sending nil
in the to
parameter of our UIApplication.sendAction(_:,to:,from:,for:)
method. The responder hierarchy will be walked to see if anyone responds to the given selector.
- iOS will walk each member of the responder chain and calls
responds(to:)
on each member. Both UIView
and UIViewController
inherit from UIResponder
so they play in that chain.
- When the system finds a responder for the selector, it will be called with that method.
While our example here is as trivial as it gets, hopefully you can see some of the hidden power. We could have a table view (like they have in the Black Pixel post I linked to above), and each cell has a button. Tapping a cell’s button sends the action to the system, which will dispatch to the view controller and handle that action.
There are 2 components from the Black Pixel example that I copied because of how good they are:
- I LOVE the
Selector
extension. This makes it super easy to reason about at the call site, and tucks the implementation bits away so that my caller need not know about what the selector is. Imagine having multiple of these call sites with the same #selector...
syntax. This is so much nicer.
- I really like using a
UIEvent
subclass to send the data I need. In hindsight this is an obvious thing (because the event
argument has to be of that subclass) but at first it didn’t occur to me.
So now we have a good way of sending data using an event from one place to another, without having to pipe anything crazy through the system. 🎉
While this technique is the right tool for a certain kind of job, it also has some sharp edges to be aware of. Here’s what I’ve noticed:
- The selector being used must have the
@objc
decoration. This is to expose the method to the Objective-C runtime where all the “magic” happens.
- The selector on our view controller could have 0, 1, or 2 parameters. However they must be ordered properly:
AnyObject
, then UIEvent
. If you try to go with a single parameter of just a UIEvent
you will get a runtime (funtime) crash.
- You must have an argument in the
from
parameter in the sendEvent
call. Otherwise the selector you pass will never be called
- However, you can mess with the
trigger
definition. You could omit the arguments entirely and it will still work. While Selector
is a type, I believe it’s just a compile-time thing. At runtime it compiles down to a string which is passed into the Objective-C performSelector:
method call. This blows my mind.
While Swift is taking the iOS developer world by storm, I think we shouldn’t neglect the tools we have at our disposal. The responder chain is an incredibly useful API to know how to use. If you’ve been in a situation where you are passing callbacks down a chain, or delegating every table view cell for a callback I highly suggest giving this technique a try. You might just be amazed at how much code you can delete.
Ask an experienced Cocoa developer about the responder chain and I bet they’ll sing its praises. However, if you ask an iOS developer about it you might be greeted with a confused shrug. I myself have had sparing opportunities to use it. So let’s ask a first question: what’s it for?
My take of things is that the responder chain is a good way for disparate objects to talk to one another. Think of a view deep in the hierarchy wanting to send some message to a superview. My particular use case has a table view cell needing to talk with the view controller containing the table view itself. There are a few ways we can do this without the responder chain. A non-exhaustive list includes: callback closures passed down (very in-style for modern iOS development), using a delegate, or an old-fashioned Notification
(née NSNotification
).
So we know the problem we’re trying to solve. How does the responder chain work? The main method we’ll be working with is on UIApplication
.
func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool
Before we dive in, there’s another post worth your reading on the Black Pixel blog from a few years ago. I’ll be rehashing some of their post here, and hopefully adding some helpful details as well.
Let’s start with a code sample so we can be on the same page. You should be able to paste this into a brand new project and have it work (assuming Xcode 10 and Swift 4.2 here).
extension Selector {
static let trigger = #selector(ViewController.somethingHappenedForAResponder(sender:forEvent:))
}
final class HandledEvent: UIEvent {
let title: String
init(title: String) {
self.title = title
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Press Me", for: .normal)
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
view.addSubview(button)
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
@objc
func somethingHappenedForAResponder(sender: AnyObject?, forEvent event: HandledEvent) {
print("triggered event named \(event.title)")
}
@objc
func buttonPressed() {
print("button pressed")
let event = HandledEvent(title: "Foo")
UIApplication.shared.sendAction(.trigger, to: nil, from: self, for: event)
}
}
Most of this code is boilerplate to add the button and handle its touch. The chain of events is button press -> responder chain -> view controller triggered. Let’s look at it piece by piece.
- The button is tapped, calling
buttonPressed()
on our view controller - The view controller creates a new instance of
HandledEvent
and calls the responder chain. This is triggered by sendingnil
in theto
parameter of ourUIApplication.sendAction(_:,to:,from:,for:)
method. The responder hierarchy will be walked to see if anyone responds to the given selector. - iOS will walk each member of the responder chain and calls
responds(to:)
on each member. BothUIView
andUIViewController
inherit fromUIResponder
so they play in that chain. - When the system finds a responder for the selector, it will be called with that method.
While our example here is as trivial as it gets, hopefully you can see some of the hidden power. We could have a table view (like they have in the Black Pixel post I linked to above), and each cell has a button. Tapping a cell’s button sends the action to the system, which will dispatch to the view controller and handle that action.
There are 2 components from the Black Pixel example that I copied because of how good they are:
- I LOVE the
Selector
extension. This makes it super easy to reason about at the call site, and tucks the implementation bits away so that my caller need not know about what the selector is. Imagine having multiple of these call sites with the same#selector...
syntax. This is so much nicer. - I really like using a
UIEvent
subclass to send the data I need. In hindsight this is an obvious thing (because theevent
argument has to be of that subclass) but at first it didn’t occur to me.
So now we have a good way of sending data using an event from one place to another, without having to pipe anything crazy through the system. 🎉
While this technique is the right tool for a certain kind of job, it also has some sharp edges to be aware of. Here’s what I’ve noticed:
- The selector being used must have the
@objc
decoration. This is to expose the method to the Objective-C runtime where all the “magic” happens. - The selector on our view controller could have 0, 1, or 2 parameters. However they must be ordered properly:
AnyObject
, thenUIEvent
. If you try to go with a single parameter of just aUIEvent
you will get a runtime (funtime) crash. - You must have an argument in the
from
parameter in thesendEvent
call. Otherwise the selector you pass will never be called - However, you can mess with the
trigger
definition. You could omit the arguments entirely and it will still work. WhileSelector
is a type, I believe it’s just a compile-time thing. At runtime it compiles down to a string which is passed into the Objective-CperformSelector:
method call. This blows my mind.
While Swift is taking the iOS developer world by storm, I think we shouldn’t neglect the tools we have at our disposal. The responder chain is an incredibly useful API to know how to use. If you’ve been in a situation where you are passing callbacks down a chain, or delegating every table view cell for a callback I highly suggest giving this technique a try. You might just be amazed at how much code you can delete.