Brent Simmons has been discussing implementing a responder chain in Swift,
Joe Groff responded with a suggestion to use as?
and a protocol to
implement this behaviour, so I thought I'd have a quick stab at it on the train
to work.
You can download a playground here, or view as a gist.
I created a ResponderProtocol
with a nextResponder
property,
a walkResponderChain
function that takes an implementor of ResponderProtocol
and a callback which accepts an implementor of ResponderProtocol
and
returns a Bool
. The callback returns true
if the responder
handles the event.
walkResponderChain
recursivley checks responders until it finds one
that handles the event, or finds nil
in a reponders nextResponder
variable.
protocol ResponderProtocol {
var nextResponder: ResponderProtocol? { get }
}
func walkResponderChain(firstResponder: ResponderProtocol?, handleEvent: (ResponderProtocol) -> Bool) -> ResponderProtocol? {
guard let responder = firstResponder else {
return nil
}
guard handleEvent(responder) == false else {
// Responder handled our call
return responder
}
return walkResponderChain(responder.nextResponder, handleEvent: handleEvent)
}
Now I can define CopyProtocol
and a copy
function. The copy
function calls walkResponderChain
and checks each object to see if it
implements its required CopyProtocol
, and if so calls instances copy
method, and returns true
to tell walkResponderChain
to stop checking
responders.
protocol CopyProtocol {
func copy()
}
func copy(firstResponder: ResponderProtocol) -> ResponderProtocol? {
return walkResponderChain(firstResponder) { responder in
if let validResponder = responder as? CopyProtocol {
validResponder.copy()
return true
}
return false
}
}
Here is the full source for the playground.
// MARK: Protocols
protocol ResponderProtocol {
var nextResponder: ResponderProtocol? { get }
}
protocol CopyProtocol {
func copy()
}
// MARK: Concrete responder
class Responder: ResponderProtocol {
var nextResponder: ResponderProtocol?
}
// MARK: Responder chain classes
class Button: Responder {}
class View: Responder {}
class CopyableView: Responder, CopyProtocol {
func copy() {
print("Copied copyable view")
}
}
class Window: Responder, CopyProtocol {
func copy() {
print("Copied window")
}
}
// MARK: Walk responder chain helper
func walkResponderChain(firstResponder: ResponderProtocol?, handleEvent: (ResponderProtocol) -> Bool) -> ResponderProtocol? {
guard let responder = firstResponder else {
return nil
}
guard handleEvent(responder) == false else {
// Responder handled our call
return responder
}
return walkResponderChain(responder.nextResponder, handleEvent: handleEvent)
}
// MARK: Copy via responder chain
func copy(firstResponder: ResponderProtocol) -> ResponderProtocol? {
return walkResponderChain(firstResponder) { responder in
if let validResponder = responder as? CopyProtocol {
validResponder.copy()
return true
}
return false
}
}
// Test case
let button = Button()
let view = View()
let copyView = CopyableView()
let window = Window()
// Setup responder chain as Button -> CopyableView -> Window
button.nextResponder = copyView
view.nextResponder = window
copyView.nextResponder = window
copy(button) // CopyableView
// Setup responder chain as Button -> View -> Window
button.nextResponder = view
copy(button) // Window