Last active
March 1, 2022 23:45
-
-
Save anandabits/ec26f67f682093cf18b170c21bcf433e to your computer and use it in GitHub Desktop.
A minimalist responder chain implemented in pure Swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Created by Matthew Johnson on 5/28/16. | |
// Copyright © 2016 Anandabits LLC. All rights reserved. | |
// | |
// This is a minimalist implementation of a responder chain in pure Swift. | |
// | |
// It is not intended to demonstrate the best way to | |
// implement event processing in Swift. | |
// | |
// The intent is to show how little code is necessary to acheive behavior | |
// similar to Cocoa's responder chain in pure Swift. | |
// | |
// There is not not a switch statement or dispatch table anywhere in this code. | |
// | |
// This example focuses on sending actions up the responder chain. | |
// Actions were chosen because they are open to extension by users. | |
// Touch, mouse, and keyboard event processing could also be implemented | |
// using similar techniques with protocols such as `TouchHandler`, etc. | |
// Instead of using a selector directly, messages are reified as instances | |
// of types conforming to the `Message` protocol. | |
// `Message` types should have value semantics. | |
// | |
// Each `Message` knows how to send itself to a `Handler`. | |
// `Handler` will usually be a protocol, but this is not required. | |
// | |
// Defining the `Message` type and `Handler` protocol is a bit more work | |
// than just using a selector. | |
// | |
// The benefits of that additional work are: | |
// * Static conformance checking for `Handler` types. | |
// * The opportunity to attach any data we wish to the event. | |
// * No need to dynamically check the types of arguments handler. | |
public protocol Message { | |
associatedtype Handler | |
// `sendToHandler` is intended to be called | |
// by framework code. | |
// User code would usually only implement this | |
// in custom `Message` types. | |
func sendToHandler(_ handler: Handler) | |
} | |
// `Responder` types are only required to provide a `nextResponder` getter. | |
// They are not required to actually handle any messages | |
// directly or through the responder chain. | |
public protocol Responder { | |
var nextResponder: Responder? { get } | |
} | |
// `Responder` types should not implement `tryToHandle`. | |
// It is an extension method in order to ensure correct | |
// (framework defined) dispatch always happens. | |
// | |
// An alternative design could make this a defualt implementation | |
// of a `tryToHandle` requirement in the protocol | |
// and provide a way to call the framework-provided | |
// implementation if necessary. | |
public extension Responder { | |
func tryToHandle<MessageType: Message>(_ message: MessageType) -> Bool { | |
return message.tryToSendTo(self) | |
} | |
func canHandle<MessageType: Message>(_ message: MessageType) -> Bool { | |
return message.canSendTo(self) | |
} | |
func tryToHandle<ActionType: Action>(_ action: ActionType, fromSender sender: ActionType.Sender) -> Bool { | |
return action.sendFrom(sender, to: self) | |
} | |
} | |
// `Action` types are like `Message` types but the `sender` | |
// is provoded to the action when it is asked to send itself | |
// to the `Handler`. | |
// Actions will usually forward the `sender` along to their | |
// `Handler` but that is not required. | |
// `Action` types should have value semantics. | |
public protocol Action { | |
associatedtype Sender | |
associatedtype Handler | |
// `performWithHandler` is intended to be called | |
// by framework code. | |
// User code would usually only implement this | |
// in custom `Action` types. | |
func performWithHandler(_ handler: Handler, sender: Sender) | |
} | |
// Type erased wrapper. | |
// This won't be necessary when Swift has generalized existentials | |
public struct AnyAction<Sender> { | |
private let action: AnyActionBase<Sender> | |
init<ActionType: Action where ActionType.Sender == Sender>(_ action: ActionType) { | |
self.action = AnyActionWrapper(action) | |
} | |
} | |
// `Control` is a minimal protocol for target action style event handling. | |
// It is relatively straightforward to extend it to support | |
// multiple target action pairs, different control events, etc. | |
// In a future version of Swift it might also be possible for | |
// `Control` to be a mixin and provide its own storage for | |
// `target` and `action`. | |
public protocol Control { | |
var target: Responder? { get } | |
var action: AnyAction<Self> { get } | |
} | |
// `Control` types should not implement `performAction`. | |
public extension Control { | |
func performAction() { | |
let responder = target ?? lookupCurrentFirstResponder() | |
action.sendFrom(self, to: responder) | |
} | |
} | |
// `Button` is a very simple concrete `Control` used to | |
// demonatrate how to use `Control`. | |
public final class Button: Control { | |
public var target: Responder? | |
public var action: AnyAction<Button> | |
public init<ActionType: Action where ActionType.Sender == Button>(target: Responder?, action: ActionType) { | |
self.target = target | |
self.action = AnyAction(action) | |
} | |
public func click() { | |
performAction() | |
} | |
} | |
///////////////////////////////////////////////////// | |
///////////////////////////////////////////////////// | |
// // | |
// Everything above this is the public interface // | |
// // | |
// Everything below this is private implementation // | |
// // | |
///////////////////////////////////////////////////// | |
///////////////////////////////////////////////////// | |
// The following private extensions are used by `Responder` | |
// to dispatch through the `Message` and by `Control` | |
// to dispatch through the `Action`. | |
// This is necessary because only concrete types conforming tp | |
// `Message` and `Action` actually know how to call the correct | |
// method on the `Handler`. | |
private extension Message { | |
func tryToSendTo(_ firstResponder: Responder) -> Bool { | |
guard let handler: Handler = findHandlerInChainStartingWith(firstResponder) | |
else { return false } | |
sendToHandler(handler) | |
return true | |
} | |
func canSendTo(_ firstResponder: Responder) -> Bool { | |
let handler = findHandlerInChainStartingWith(firstResponder) as Handler? | |
return handler != nil | |
} | |
} | |
private extension Action { | |
func sendFrom(_ sender: Sender, to responder: Responder) -> Bool { | |
guard let handler: Handler = findHandlerInChainStartingWith(responder) | |
else { return false } | |
performWithHandler(handler, sender: sender) | |
return true | |
} | |
} | |
private extension AnyAction { | |
func sendFrom(_ sender: Sender, to firstResponder: Responder) -> Bool { | |
return action.sendFrom(sender, to: firstResponder) | |
} | |
} | |
// This implements the handler lookup loop that looks for | |
// an appropriate `Handler` in the `Responder` chain. | |
// | |
// The example only shows one dispatch algorithm. | |
// Alternative dispatch algorithms could be implemented over | |
// any data structure containing `Responder` instances | |
// allowing for arbitrarily complex dispatch logic. | |
private func findHandlerInChainStartingWith<Handler>(_ firstResponder: Responder) -> Handler? { | |
var nextResponder: Responder? = firstResponder | |
while let responder = nextResponder { | |
if let handler = responder as? Handler { | |
return handler | |
} | |
print("\(responder) cannot handle the message") | |
nextResponder = responder.nextResponder | |
} | |
return nil | |
} | |
// `firstResponder` is defined as a variable in user code | |
// near the bottom of this file. | |
// In a real framework it would be tracked by the framework | |
// as part of the application state. | |
private func lookupCurrentFirstResponder() -> Responder { | |
return firstResponder | |
} | |
// These types only exist to perform type erasure for `AnyAction`. | |
// When Swift introduces generalized existentials they will not | |
// be necessary. | |
/* abstract */ private class AnyActionBase<Sender> { | |
func sendFrom(_ sender: Sender, to firstResponder: Responder) -> Bool { | |
fatalError("abstract method AnyMessageBase.sendFrom:to: called") | |
} | |
} | |
private final class AnyActionWrapper<ActionType: Action>: AnyActionBase<ActionType.Sender> { | |
let action: ActionType | |
init(_ action: ActionType) { | |
self.action = action | |
} | |
override func sendFrom(_ sender: ActionType.Sender, to firstResponder: Responder) -> Bool { | |
return action.sendFrom(sender, to: firstResponder) | |
} | |
} | |
///////////////////////////////////////////////////// | |
///////////////////////////////////////////////////// | |
// // | |
// Everything above this is framework level code // | |
// // | |
// Everything below this is application level code // | |
// // | |
///////////////////////////////////////////////////// | |
///////////////////////////////////////////////////// | |
// A simple action message only requires a protocol | |
// and a function that dispatches to the handler. | |
protocol Fooable { | |
func foo() | |
} | |
struct FooAction: Message { | |
func sendToHandler(_ handler: Fooable) { | |
handler.foo() | |
} | |
} | |
// More complex actions are also possible. | |
// These capture data and forward it to the handler | |
// with full static type checking in force. | |
// This is much better than needing to use the same | |
// signature (or limited set of signatures) for all actions. | |
// It avoids the need to make assertions about and / or cast | |
// the arguments in the way that is necessary in Cocoa. | |
// See: http://blog.wilshipley.com/2016/05/pimp-my-code-book-2-swift-and-dynamism.html | |
// for an example of this assertion problem. | |
protocol Barable { | |
func bar(arg1: String, arg2: Int, arg3: Bool, arg4: Double) | |
} | |
struct BarAction: Message { | |
var arg1: String, arg2: Int, arg3: Bool, arg4: Double | |
func sendToHandler(_ handler: Barable) { | |
handler.bar(arg1: arg1, arg2: arg2, arg3: arg3, arg4: arg4) | |
} | |
} | |
// A basic `Responder` that doesn't handle any messages. | |
class BasicResponder: Responder { | |
var nextResponder: Responder? | |
init(nextResponder: Responder? = nil) { | |
self.nextResponder = nextResponder | |
} | |
} | |
// `Responder` types that wish to handle messages are only | |
// required to conform to the `Handler` protocol associated with | |
// the `Message` types they wish to handle. | |
// | |
// The conformance is statically verified. | |
// The handler method receives statically typed arguments | |
// avoiding the need for any assertions and / or casts. | |
class FooResponder: Responder, Fooable { | |
var message: String | |
var nextResponder: Responder? | |
init(message: String, nextResponder: Responder? = nil) { | |
self.message = message | |
self.nextResponder = nextResponder | |
} | |
func foo() { | |
print(message) | |
} | |
} | |
// Put together a simple responder chain: | |
let topLevelResponder = BasicResponder() | |
let fooResponder = FooResponder(message: "FooResponder got it!", nextResponder: topLevelResponder) | |
let firstResponder = BasicResponder(nextResponder: fooResponder) | |
// Query the responder chain to find out that a `FiiAction` can be handled | |
// by the current dynamic responder chain. | |
let fooCanBeHandled = firstResponder.canHandle(FooAction()) | |
print(fooCanBeHandled) | |
/* | |
BasicResponder cannot handle the message | |
true | |
*/ | |
// Send a `FooAction` and observe that it is handled by | |
// the current dynamic responder chain. | |
let fooHandled = firstResponder.tryToHandle(FooAction()) | |
print(fooHandled) | |
/* | |
BasicResponder cannot handle the message | |
FooResponder got it! | |
true | |
*/ | |
// Query the responder chain to find out that a `BarAction` *cannot* be handled | |
// by the current dynamic responder chain. | |
let barCanBeHandled = firstResponder.canHandle(BarAction(arg1: "arg", arg2: 42, arg3: true, arg4: 42)) | |
print(barCanBeHandled) | |
/* | |
BasicResponder cannot handle the message | |
FooResponder cannot handle the message | |
BasicResponder cannot handle the message | |
false | |
*/ | |
// Try to send a `BarAction` even though it can't be handled by the current | |
// dynamic responder chain. | |
let barHandled = firstResponder.tryToHandle(BarAction(arg1: "arg", arg2: 42, arg3: true, arg4: 42)) | |
print(barHandled) | |
/* | |
BasicResponder cannot handle the event | |
FooResponder cannot handle the event | |
BasicResponder cannot handle the event | |
false | |
*/ | |
// The custom `Handler` protocol and `Action` struct | |
// are necessary boilerplate that is not required by Cocoa. | |
// However, it is easy to imagine a future version of Swift | |
// where they can be automatically synthesized during compilation | |
// with an annotation as concise as `@IBAction`. | |
// It is even possible that we may be able to implement synthesis-invoking | |
// annotations like this in user-defined code (or some similarly concise syntax). | |
protocol CustomActionable { | |
func customAction(sender: Button) | |
} | |
struct CustomAction: Action { | |
func performWithHandler(_ handler: CustomActionable, sender: Button) { | |
handler.customAction(sender: sender) | |
} | |
} | |
class CustomView: Responder, CustomActionable { | |
var nextResponder: Responder? | |
var button: Button | |
init(sendActionToResponderChain: Bool = false) { | |
button = Button(target: nil, action: CustomAction()) | |
button.target = sendActionToResponderChain ? nil : self | |
} | |
// Custom, strongly typed action method. | |
// See: http://blog.wilshipley.com/2016/05/pimp-my-code-book-2-swift-and-dynamism.html | |
// to understand why the strong typing here is an important step forward. | |
// | |
// In a future version of Swift it might be possible to invoke | |
// synthesis of an anonymous `Handler` protocol and `Action` struct | |
// using the annotation we are already using in our code today. | |
// @IBAction | |
func customAction(sender: Button) { | |
print("\(sender) sent message to \(self)") | |
} | |
} | |
// Create an instance of the view and simulate a button click. | |
let view = CustomView() | |
view.button.click() | |
/* | |
Button sent message to CustomView | |
*/ | |
// Create an instance of the view that sends the click action up | |
// the responder chain. | |
let upResponderChain = CustomView(sendActionToResponderChain: true) | |
upResponderChain.button.click() | |
/* | |
Platinum.BasicResponder cannot handle the message | |
Platinum.FooResponder cannot handle the message | |
Platinum.BasicResponder cannot handle the message | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment