Fat GrinCatchlines, Articles, About

Swift responder chain

16 May 2016

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

— Ryan Gibson —