An ode to the responder chain ❤️

The responder chain doesn’t get a whole lot of love anymore. It sits on the sidelines as ol’ dependable, helping your app out under the hood while rarely being the first choice of communication for your UI events within your app.

In the early days of iPhone OS (as it was called back then) the responder chain was the easy way to communicate UI events outside of your view or view controller without having to explicitly name a target, delegate or notification and all the associated code & setup that came with those approaches. Sure sometimes events would get lost due to programmer error, but if after a refactoring and your button stopped working having been tapped you had an “oh… right… missed that” moment and quickly fixed it.

Then something happened… the iPad came out with all of these fancy, new gesture recognizers allowing you to easily know when a tap, rotation, pinch or zoom occured, with a whole new way of knowing when those events occurred. It kicked the responder chain right to the curb, only communicating though explicit target-action pairs.

These were the new hotness. Everybody started using it and gesture recognizers being a lot more easy to learn quickly became the default for new people getting acquainted with iOS development. Why put in all this effort to learn about next responder, the responder chain, making sure your view controllers are properly a child of their parent view controller and all that jazz if you can just set a target-action pair and ship?

This changed something though! The responsibility of setting up a view to emit an event shifted from being purely encapsulated within that view or view controller to having to be set up from outside of that view or view controller. While not bad it could be overkill if you just want to signal an event, and can lead to slightly coupled code.

Let’s do a somewhat contrived example. A common requirement within apps is to ask the customer for permission to send push notifications. This is a good example as using the responder chain is great when you need to know an event happened within your app, but don’t care about what sent the event. You decide to ask for permission in your onboarding flow with the user having tapped a custom checkmark that is just a view with a tap gesture recognizer attached.

Your code might look like this:

class OnboardingView: UIView {
    let enableNotificationsCheckmarkView: CheckmarkView
    ...
}

class OnboardingViewController: UIViewController {
    private let onboardingView = OnboardingView()

    override func loadView() {
        self.view = onboardingView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let enableNotificationsRecognizer = UITapGestureRecognizer(target: self,
                                                                   action: #selector(handleEnableNotifications(_:)))

        onboardingView.enableNotificationsCheckmarkView.addGestureRecognizer(enableNotificationsRecognizer)
    }

    @objc func handleEnableNotifications(_ recognizer: UIGestureRecognizer) {
        // Ask user for permission for notifications
        ...
    }
}

Pretty simple… but then a business requirement comes up to also put this in the more menu. Now you’ve got some choices to make… Do you create a NotificationsManager type object and call that from within handleEnableNotifications(_:)? Now your OnboardingViewController needs to know about NotificationsManager as does your more menu code. If you’re into dependency injection that comes with some extra complexity.

Let’s consider a delegate. You’ll have to define a protocol, create a weak delegate in your OnboardingViewController, call that delegate from handleEnableNotifications(_:) and do something similar in your more menu code. Depending on where you are setting the delegate from you may have to break some encapsulation as well. If you’ve found yourself making a delegate for a delegate to get around this… you’ve delved too deep and monsters await. 👻 Similar issues will occur if you decide to break encapsulation and add a tap gesture recognizer from outside of the OnboardingViewController, and ditto if you pass in a callback.

Let’s consider a notification. This isn’t bad but you’ll still need to have this tap gesture recognizer fire the notification, have the notification name defined, and have registered to receive those notifications. This isn’t brittle, but there’s a way with even less code.

Wouldn’t it be nice if our onboarding view could just shout to the world (or app), “Hey! Gimme some notification permissions! I don’t care how it’s done!”. If our checkmark was a UIButton we could actually do this:

class OnboardingView: UIView {
    let enableNotificationsCheckmarkButton: CheckmarkButton
    ...
}

class OnboardingViewController: UIViewController {
    let onboardingView = OnboardingView()

    override func loadView() {
        self.view = onboardingView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        onboardingView.enableNotificationsCheckmarkButton.addTarget(nil,
                                                                    action: Selector(("handleEnableNotifications")),
                                                                    for: .primaryActionTriggered)
    }
}

This will walk the responder chain from the button, to the OnboardingView, to the OnboardingViewController, to its parent view controller, to the UIApplication object, and then finally to the UIApplicationDelegate, checking each object if it responds to the method handleEnableNotifications. This link has a very good description.

Just to be complete our AppDelegate may look like this:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    @objc func handleEnableNotifications() {
        // Handle our notification permission query
        ...
    }
}

People into compile time safety are probably having 🚨 big red warning signals 🚨 flashing in their mind right now with the use of Selector(("handleEnableNotifications")). Swift is all about compile time safety, and with a bit of extra code we can get that, otherwise we run the risk of fat-fingering the method to call when that button is tapped, and also the implementation. This was a risk back in early iOS, but thankfully we can rely on the swift compiler to warn us against those incidents.

Here’s how all the code would look with proper compile time safety:

    @objc protocol EnableNotificationsResponder: AnyObject {
    func handleEnableNotifications()
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, EnableNotificationsResponder {

    func handleEnableNotifications() {
        // Handle our notification permission query
        ...
    }
}

class OnboardingView: UIView {
    let enableNotificationsCheckmarkButton: CheckmarkButton
    ...
}

class OnboardingViewController: UIViewController {
    let onboardingView = OnboardingView()

    override func loadView() {
        self.view = onboardingView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        onboardingView.enableNotificationsCheckmarkButton.addTarget(nil,
                                                                    action: #selector(EnableNotificationsResponder.handleEnableNotifications),
                                                                    for: .primaryActionTriggered)
    }
}

Our more menu can do something similar, and as long as it sends the same call up the responder chain it will be handled in the same way. When version 2.0 of your app comes out with a completely redesigned interface you won’t be messing with delegate hookups, callbacks having to be injected, and new classes having to be aware of a potential NotificationsManager. Just fire up the responder chain and forget.

Sometimes we are not able to use a UIButton or related control without a target that uses the responder chain. You can fire your own events using ‘UIApplication.shared.sendAction’. Let’s see how you would do that if you only wanted to fire up the responder chain when a touch ended on a view:

    class SecondOnboardingView: UIView {
    ...

    override func touchesEnded(_ touches: Set, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)

        UIApplication.shared.sendAction(#selector(EnableNotificationsResponder.handleEnableNotifications(_:)), to: nil, from: self, for: nil)
    }
}

class SecondOnboardingViewController: UIViewController {

    override func loadView() {
        let view = UIView()

        view.backgroundColor = .white

        let secondOnboardingView = SecondOnboardingView()
        secondOnboardingView.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(secondOnboardingView)

        NSLayoutConstraint.activate([
            secondOnboardingView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
            secondOnboardingView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5),
            secondOnboardingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            secondOnboardingView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])

        self.view = view
    }
}

These are easy to test too as it is so easy to wrap the button, view or view controller that sends these event in something that is itself part of the responder chain. You just need to have something wrap the UIResponder that fires the event, and then capture it. Best of all… no mocking! Check it out:

    class OnboardingViewControllerTest: XCTestCase {

    var subject: ResponderCatcher!
    var onboardingViewController: OnboardingViewController!

    override func setUp() {
        subject = ResponderCatcher()

        onboardingViewController = OnboardingViewController()

        subject.addChild(onboardingViewController)
        subject.view.addSubview(onboardingViewController.view)
        onboardingViewController.didMove(toParent: subject)
    }

    func testOnboardingCheckmarkButton() {
        onboardingViewController.onboardingView.enableNotificationsCheckmarkButton.sendActions(for: .primaryActionTriggered)

        assert(subject.caughtResponderAction, "Expected to have caught our responder action")
    }
}


class ResponderCatcher: UIViewController, EnableNotificationsResponder {
    var caughtResponderAction = false

    func handleEnableNotifications(_ sender: Any?) {
        caughtResponderAction = true
    }
}

There is so much you can do with the responder chain. You can define responder actions in interface builder, modify the responder chain away from it’s default flow, make it do your laundry… (almost…) It definitely should be under consideration when you build an app.