Skip to content

Instantly share code, notes, and snippets.

@roop
Created June 3, 2016 21:49
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save roop/397e4cc5e7deeab6a896ef4034a06ae5 to your computer and use it in GitHub Desktop.
Save roop/397e4cc5e7deeab6a896ef4034a06ae5 to your computer and use it in GitHub Desktop.
A responder chain implementation for a hypothetical pure-Swift equivalent for UIKit. More info at: http://roopc.net/posts/2016/swifty-responder-chain/
/* A responder chain implementation for a hypothetical pure-Swift
* equivalent for UIKit.
* More info at: http://roopc.net/posts/2016/swifty-responder-chain/
*/
/* A responder is something that has an optional next responder,
so that we can have a chain of responders. */
protocol Responder {
var nextResponder: Responder? { get }
}
/* A command needs to be able to define methods that take in the
instances of the corresponding responder sub-type as an
argument, so we need to set the corresponding responder type
as an associated type. */
protocol Command {
associatedtype AssociatedResponder
func canPerformOnResponder(responder: AssociatedResponder) -> Bool
func performOnResponder(responder: AssociatedResponder)
}
/* To enable views to be included in the responder chain, you can
extend Responder and return whatever is appropriate as the
next responder. */
class View {
var superview: View? = nil
}
extension View: Responder {
var nextResponder: Responder? { return self.superview }
}
/* The application knows what the first responder is, and it can
traverse the responder chain querying each responder in the
process. */
class Application {
var firstResponder: Responder? = nil
func performCommand<C: Command>(command: C) {
var r: Responder? = self.firstResponder
while r != nil {
if let r = r {
if command.canPerformOnResponder(r) {
command.performOnResponder(r)
return
}
}
r = r?.nextResponder
}
}
}
/* The calls to canPerformOnResponder() and performOnResponder()
above pass a Responder argument, but the Command protocol only
declares methods that take in AssociatedResponder instances.
So to make the above code work, we need to generalize those
methods to take in any Responder. */
extension Command {
func canPerformOnResponder(responder: Responder) -> Bool {
if let associatedResponder = responder as? AssociatedResponder {
return self.canPerformOnResponder(associatedResponder)
}
return false
}
func performOnResponder(responder: Responder) {
if let associatedResponder = responder as? AssociatedResponder {
self.performOnResponder(associatedResponder)
}
}
}
/* Now, we can start defining commands. */
// Copy
protocol CopyResponder: Responder {
func canCopy() -> Bool
func copy()
}
struct CopyCommand: Command {
func canPerformOnResponder(responder: CopyResponder) -> Bool {
return responder.canCopy()
}
func performOnResponder(responder: CopyResponder) {
responder.copy()
}
}
// Go Fishing
protocol GoFishingResponder: Responder {
func canGoFishing() -> Bool
func goFishing()
}
struct GoFishingCommand: Command {
func canPerformOnResponder(responder: GoFishingResponder) -> Bool {
return responder.canGoFishing()
}
func performOnResponder(responder: GoFishingResponder) {
responder.goFishing()
}
}
/* We can make views respond to these commands. */
class MyView: View {
let name: String
var isCopyable = false
var hasFishingLine = false
init(name: String) {
self.name = name
}
}
extension MyView: CopyResponder {
func canCopy() -> Bool {
return self.isCopyable
}
func copy() {
print("\(self.name) copying")
}
}
extension MyView: GoFishingResponder {
func canGoFishing() -> Bool {
return self.hasFishingLine
}
func goFishing() {
print("\(self.name) going fishing")
}
}
/* We can wire up views into a responder chain and
check how commands propagate through the chain */
let topView = MyView(name: "topView")
let midView = MyView(name: "midView")
midView.superview = topView
let lowView = MyView(name: "lowView")
lowView.superview = midView
let app = Application()
app.firstResponder = lowView
midView.isCopyable = false
midView.hasFishingLine = true
topView.isCopyable = true
topView.hasFishingLine = true
app.performCommand(CopyCommand()) // "topView copying"
app.performCommand(GoFishingCommand()) // "midView going fishing"
midView.isCopyable = true
midView.hasFishingLine = false
topView.isCopyable = true
topView.hasFishingLine = true
app.performCommand(CopyCommand()) // "midView copying"
app.performCommand(GoFishingCommand()) // "topView going fishing"
/* The End */
@jakubpetrik
Copy link

The advantages (if any) are at least doubtful, but using the SequenceType:

struct ResponderChain: SequenceType {
    typealias Generator = ResponderGenerator
    let first: ResponderGenerator.Element?
    func generate() -> Generator {
        return ResponderGenerator(current: first)
    }
}

struct ResponderGenerator: GeneratorType {
    typealias Element = Responder
    var current: Element?
    mutating func next() -> Element? {
        current = current?.nextResponder
        return current
    }
}

performCommand can be "simplified" to:

class Application {
    var firstResponder: Responder? = nil
    func performCommand<C: Command>(command: C) {
        guard let r = ResponderChain(first: firstResponder).lazy.filter(command.canPerformOnResponder).first else { return }
        command.performOnResponder(r)
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment