Swifty Responder Chain
From Jordan Rose’s tweets in response to my previous post (his responses are inlined in the post), it’s clear there’s no guarantee that Swift will get an Objective-C-ish runtime (there’s no guarantee it won’t either). So I’m trying to approach the problem from the other side: What do we need in Swift to create UIKit-like “magic interfaces” without swinging all the way to the dynamic side, à la Objective-C. For a start, let’s see if we can create a responder chain for a hypothetical pure-Swift equivalent for UIKit.
Brent Simmons’ first go at a pure-Swift responder chain
requires us to handle all commands in one respondsToCommand:
method
with a switch statement to select between commands, which is
no good. Joe Groff
suggested we use protocol conformance, but that
still requires a performSelector:
, which the Swift
runtime might not give us. So what can we do with what we have, and
with what we’re more likely to get?
A better interface
Building on that suggestion by Joe Groff, if we
can’t invoke selectors directly, we can introduce a new Command
type
to abstract that out. The Command
type gives us a uniform interface
to perform a command on a corresponding Responder
object. (This
interface is conceptually similar to what Matthew Johnson came up
with, but is simpler and therefore hopefully
easier to wrap our heads around.)
With this interface, whenever we introduce a new command, we have to define two new types:
- A responder: A sub-protocol of
Responder
declaring the methods used to respond to that command - A command: A concrete subtype of
Command
that provides a uniform interface using the methods declared in the responder. This is what will get associated with a menu item or an equivalent thing that would “launch” the command.
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()
}
}
To enable an object to respond to the CopyCommand
, all we have to do
is to extend CopyResponder
and implement its methods.
extension MyView: CopyResponder {
func canCopy() -> Bool {
return true
}
func copy() {
print("Copying from MyView")
}
}
Query functions like canCopy()
make it possible for menus to be
enabled / disabled based on the state of the app at runtime (like what
Gus Mueller does with Acorn), by having responder objects
change their answer at runtime.
With this interface, the app developer doesn’t have to write any switch
statements to route commands. There’s no performSelector:
-like runtime
magic either.
Behind the scenes
There’s a bunch of framework code we need for making it possible for the app developer to define commands and responders like above.
To start simple, 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
}
}
}
We cannot take an instance of Command
as an argument directly, because
Command
has an associated type, so we use generics to define
performCommand()
– C
is a placeholder type that conforms to
the Command
protocol.
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)
}
}
}
And that’s it. Now we have all the pieces in place to enable us to define commands, declare corresponding responders and make arbitrary types behave as those responders.
This pattern can be extended for framework events as well:
protocol HandleTouchResponder: Responder {
func canHandleTouch(touches: [Touch]) -> Bool
func handleTouch(touches: [Touch])
}
struct HandleTouchCommand: Command {
let touches: [Touch] = []
func canPerformOnResponder(responder: HandleTouchResponder) -> Bool {
return responder.canHandleTouch(self.touches)
}
func performOnResponder(responder: HandleTouchResponder) {
responder.handleTouch(self.touches)
}
}
Where’s the switch?
So how does the command dispatch happen in the above architecture?
Since we always start command propagation with a command whose concrete command type is known, we can always get to its associated type. From the associated type, we know how to perform the command on a responder object that conforms to the associated type. That’s how we’re able to get away without a switch – we’re using the commands as a sort-of distributed dispatch table.
Furthermore
I’m suprised we can get this far towards a usable responder chain with the current state of Swift, but with improvements to the language, we can do better.
Constraining the associated responder
We should be able to tell Swift that the associated type has to conform
to Responder
, like this:
protocol Command {
associatedtype AssociatedResponder: Responder
func canPerformOnResponder(responder: AssociatedResponder) -> Bool
func performOnResponder(responder: AssociatedResponder)
}
but it looks like we can’t do that at present.
Protocols with associated types
Protocols with associated types have a lot of restrictions on how they can be used. One of those is that you can’t instantiate or cast to those types.
If the Swift protocol system gets cleverer, I can imagine simplifying
the interface by moving the associatedtype
to the Responder
.
protocol Command {
}
protocol Responder {
associatedtype AssociatedCommand: Command
func canPerformCommand(command: AssociatedCommand)
func performCommand(command: AssociatedCommand)
}
If Command
and Responder
could be declared like this, it would
reduce the boilerplate required for declaring new commands. But as of
Swift 2.2, with the above declaration of Responder
, we can’t have
variables of Responder
or any of its sub-protocols like
CopyResponder
, nor can we downcast to those types, so we can’t get far
with this.
Update 6/Jul/2015:
As Matthew Johnson explained to me
over Twitter, this solution is requires that a type be able to conform
to a root protocol in multiple ways (i.e. MyView
needs to conform to
Responder
through CopyResponder
as well as through
GoFishingResponder
), which the Swift team frowns upon. Therefore this
solution for a responder chain might not become feasible even in the
future.
Moreoever, this way of modelling a responder chain can be done using generic protocols:
protocol Responder<C: Command> {
func canPerformCommand(command: C)
func performCommand(command: C)
}
protocol Responder<CopyCommand> {
func canPerformCommand(command: CopyCommand)
func performCommand(command: CopyCommand)
}
extend MyView: Responder<CopyCommand> {
...
}
And Doug Gregor’s Generics Manifesto mail says generic protocols is unlikely to become part of Swift because:
[These use cases] seem too few to justify the potential for confusion between associated types and generic parameters of protocols; we’re better off not having the latter.
I think the responder chain is a good use case for generic protocols
– there’s no confusion in a type conforming to Responder
in
multiple ways. However, I think there is going to be still a problem
casting an object to Responder
, so it might not be a sufficiently good
use case.
So far so good
It’s amazing that Swift protocols can get us this far towards a
responder chain implementation (the full working code is
here). And we can already see how it can get better as Swift
gets its wrinkles ironed out with some changes to how Swift does
things.
That said, the responder chain was probably an easy start. KVO and Core Data will get progressively harder to create without dynamic language features.
This post was discussed in iOS Dev Weekly and Swift Weekly.