Skip to content

Use the UIKit responder chain as an alternative to the delegate pattern

License

Notifications You must be signed in to change notification settings

jeffbailey/ResponderChainActionMessages

Repository files navigation

Responder Chain Action Messages

SendActionDemo is a sample app that demonstrates how to use the UIKit responder chain as an alternative to the delegate pattern. It's a great option when you need looser coupling between components, but still want the type safety of delegates.

iOS Twitter: @jeffbailey

Motivation

In this sample app we're building a tab bar based application, where the first tab is a dashboard that provides summary information for each of the other tabs. Each card in the dashboard has a "Go To" button that allows the user to quickly switch tabs.

ScrollingStackViewController Demo

The "Go To" button lives in a DashboardCollectionViewCell, but the action to switch tabs needs to be handled by the MainTabBarController, which is much higher in the view hierarchy. Instead of passing a delegate down from the tab bar controller to every cell, we take advantage of the responder chain to send an Action Message from the DashboardCollectionViewCell up to the MainTabBarController.

The Responder Chain

For background on the UIKit Responder Chain, refer to these resources:

Apple UIKit Documentation: Using Responders and the Responder Chain to Handle Events

The Amazing Responder Chain | Cocoanetics

Foundation Code

We start with a simple extension of UIResponder to send an action message to the responder chain.

extension UIResponder {
    func app_sendActionToResponderChain(_ action: Selector, sender: UIResponder,
                                        forEvent event: UIEvent? = nil) -> Bool {

        return UIApplication.shared.sendAction(action, to: nil, from: sender, for: event)
    }
}

Note that sending nil as the target in sendAction will cause UIKit to traverse the responder chain until it finds an object that implements the appropriate action selector.

Next, we create a protocol to model an Action Message, which consists of a Selector and an optional UIEvent that will be sent to the message.

protocol ActionMessage {
    var selector: Selector { get }
    var event: UIEvent? { get }
}

extension ActionMessage {
    func sendAction(from sender: UIResponder) -> Bool {
        return sender.app_sendActionToResponderChain(selector, sender: sender, forEvent: event)
    }
}

Application Code

The goal in our sample application is to open either the Map, Favorites, or Settings tab, depending on which button was pressed. That sounds like a good use for an enum based ActionMessage!

enum OpenTabActionMessage: ActionMessage {
    case openMap
    case openFavorites
    case openSettings

    var event: UIEvent? {
        switch self {
        case .openMap:
            return OpenTabEvent(tabType: .nearby)
        case .openFavorites:
            return OpenTabEvent(tabType: .favorites)
        case .openSettings:
            return OpenTabEvent(tabType: .settings)
        }
    }

    var selector: Selector {
        return .handleOpenTab
    }
}

class OpenTabEvent: UIEvent {
    enum TabType {
        case nearby, favorites, settings
    }

    let tabType: TabType

    init(tabType: TabType) {
        self.tabType = tabType
    }
}

@objc protocol OpenTabMessageHandler {
    func handleOpenTab(_sender: AnyObject, forEvent event: OpenTabEvent)
}

extension Selector {
    static let handleOpenTab = #selector(OpenTabMessageHandler.handleOpenTab(_sender:forEvent:))
}

OpenTabEvent is a simple subclass of UIEvent to tell the receiver of the message which tab to open.

Using a protocol for OpenTabMessageHandler and the Selector extension provides type safety between the sender and receiver.

All that's left is to send and receive the OpenTabActionMessage.

First, send the ActionMessage from the IBAction handler wired to the button press:

class DashboardCollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var selectTabButton: UIButton!

    var openTabActionMessage: OpenTabActionMessage?

    @IBAction func selectTabButtonTapped(_ sender: Any) {
        _ = openTabActionMessage?.sendAction(from: self)
    }
}

UIKit will traverse the responder chain until it finds an object that implements the selector to open the tab. In this case, MainTabBarController implements the OpenTabMessageHandler protocol to handle the action in a type safe manner:

extension MainTabBarController: OpenTabMessageHandler {
    func handleOpenTab(_sender: AnyObject, forEvent event: OpenTabEvent) {
        switch event.tabType {
        case .nearby:
            self.selectedViewController = self.mapViewController
        case .favorites:
            self.selectedViewController = self.favoritesViewController
        case .settings:
            self.selectedViewController = self.settingsViewController
        }
    }
}

Summary

Responder Chain Action Messages aren't a replacement for the tried and true delegate pattern used in iOS, but they can be a handy tool in your toolbox.

Download or clone the project to take a closer look.

Requirements

  • XCode 10
  • Swift 4.2

Credits

Inspired by a great talk by at the 2017 360iDev by Brandon Alexander :

https://vimeopro.com/360conferences/360idev-2017-public-session-recordings/video/231791014

License

This project is licensed under the MIT License - see the LICENSE file for details

MIT © Jeff Bailey

About

Use the UIKit responder chain as an alternative to the delegate pattern

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages