lsauceda lsauceda - 3 months ago 14
Swift Question

Generic dispatch mechanism using runtime-specified Protocol conformance test & selector

So, I am making a SpriteKit game, and I want to forward

update
,
mouseDown
,
keyDown
, etc... events that are called on the
GameScene
, to that scene's children, provided that they are a custom subclass of SKNode that has the appropriate responders.

I made several protocols depending on which functions I want to call on each node, so like I have
KeyboardDelegate
for nodes that need to receive
keyDown
,
keyUp
, etc...
UpdateDelegate
for
update
,
didSimulatePhysics
, etc... and something like that for each type of calls in total I have
UpdateDelegate
,
KeyboardDelegate
,
MouseDelegate
, and
CollisionDelegate
each for it's respective type of calls.

And so in the
GameScene
I have functions like:

func someEvent(someParameter: ArgumentType) {
for child in children where child is SomeProtocol {
//onSomeEvent should be required by SomeProtocol
(child as! SomeProtocol).onSomeEvent(someParameter)
}
}


The problem is that there are about 20 of these events that I want to forward and I have that same code in every function, the only part that varies are
SomeProtocol
and
onSomeEvent
.

I tried to make a function that handles all that so I don't need to write that code for each event I want to forward like this:

extension SKNode {
func forward<T>(method: Selector, onChildrenOfType: T.Type, withArgument argument: AnyObject) {
for child in children where child is T {
child.performSelector(method, withObject: first)
}
}
}


The extension was on SKNode so I would be able forward calls on each children of each individual node receiving the call no matter what type of SKNode they were.

The function itself didn't seem quite right, and it wasn't because it didn't work, I couldn't pass the protocol directly without using
SomeProtocol.self
and whenever I called it like this:

override func someEvent(someParameter: ArgumentType) {
self.recurse(#selector(onSomeEvent(_:)), onChildrenOfType: SomeProtocol.self, withArgument: someParameter)
}


I'd get an
use of unresolved identifier 'onSomeEvent'
, I assume because the compiler doesn't know where to look for the
onSomeEvent
function.

So... is there a way to make this work (besides probably using the old selector syntax) preferably if I can pass more that 2 parameters (since performSelector just allows for up to 2, and
objc_msgSend
and
NSInvocation
are unavailable), or should I just stick to copying the code in each event I want to forward.

Answer

So this was interesting & fairly tricky. The key is to test a runtime-protocol type passed in as a parameter -- that's rather meta, and not possible in Swift which is using compile-time conformance checking for efficiency & safety. So first of all you need to ground everything in the Obj-C runtime and the key here is the conformsToProtocol method. If you need such a highly generic dispatch, I would not try passing more than 2 arguments: instead pass in a some kind of container (like NSNotification's userInfo: NSDictionary) with the values you need. The below can easily be adapted to a function that takes 1 parameter, as I'm sure you know, by using the withObject: variant of performSelector:. Note that a selector function must return an object else your code will crash hard so I just chose self here for convenience (and chainability!)

import Foundation

// performSelector methods must return a reference to something
@objc protocol P1 {
    func doP1Action() -> P1
}
@objc protocol P2 {
    func doP2Action() -> P2
}
class C1a: NSObject, P1 {
    func doP1Action() -> P1 { print("I'm doing C1a's impl of a P1 Action"); return self }
}
class C1b: NSObject, P1 {
    func doP1Action() -> P1 { print("I'm doing C1b's impl of a P1 Action"); return self }
}
class C2a: NSObject, P2 {
    func doP2Action() -> P2 { print("I'm doing C2a's impl of a P2 Action"); return self }
}
class C2b: NSObject, P2 {
    func doP2Action() -> P2 { print("I'm doing C2b's impl of a P2 Action"); return self }
}

var children = [ C1a(), C2b(), C1b(), C1a(), C2a() ]


func performAction(action: Selector, forObjectsConformingTo conform: Protocol, inArray array: [AnyObject]) {
    (array.flatMap { $0.conformsToProtocol(conform) ? $0 : nil }).forEach { $0.performSelector(action) }
}


// And now the fully generic reusable logic:
print("Performing actions on P1's:")
performAction(#selector(P1.doP1Action), forObjectsConformingTo: P1.self, inArray: children)

print("Performing actions on P2's:")
performAction(#selector(P2.doP2Action), forObjectsConformingTo: P2.self, inArray: children)

//Performing actions on P1's:
//I'm doing C1a's impl of a P1 Action
//I'm doing C1b's impl of a P1 Action
//I'm doing C1a's impl of a P1 Action
//Performing actions on P2's:
//I'm doing C2b's impl of a P2 Action
//I'm doing C2a's impl of a P2 Action

As far as recursion, that's the easiest part and nothing to do with navigating the type calculus or the Foundation API. You just place a recursive call in the forEach body on the children of self.