Handling Keyboard Input in a View Controller – Alongside SpriteKit

April 15, 2015

I’m using SpriteKit in the train game I’ve been tinkering with on and off, and recently went to add keyboard event handling to my view controller:

override func keyDown(event: NSEvent) {
    interpretKeyEvents([event])
}

override func deleteBackward(sender: AnyObject?) {
    println("deleteBackward")
}

To my surprise, though, whatever was first responder wasn’t passing keyDown: up the responder chain. (This is 10.10 so view controllers are in the responder chain automatically.)

First, determining what was first responder was easy enough. After showWindow: had been called:

(lldb) ex windowController.window!.firstResponder
(SKView) $R2 = 0x0000000100e1b100 {

The SKView is the first responder. I had forgotten that SKNode is a subclass of NSResponder and that nodes can participate in the responder chain.

So why wasn’t the keyDown: propagating up to the view controller? I checked the nextResponder of the SKView: it was the same as its superview, which is to be expected. The question remains: why wasn’t keyDown: propagating?

For the answer to that I turned to Hopper Disassembler, which revealed that -[SKView keyDown:] essentially sends keyDown: to its scene:

void -[SKView keyDown:](void * self, void * _cmd, void * arg2) {
    rbx = self;
    r14 = [arg2 retain];
    if (rbx->_disableInput == 0x0) {
        rdi = rbx->_scene;
        if (rdi != 0x0) {
            [rdi keyDown:r14];
        }
    }
    rdi = r14;
    rax = [rdi release];
    return;
}

So how can I deal with this? I could subclass SKView and override keyDown: to give it a more traditional implementation (pass it on to nextResponder, for example). However, while I was in Hopper I noticed that SKScene doesn’t implement keyDown:, which means that it is relying on the default implementation of keyDown: in NSResponder. Which means that the simplest solution is to set the scene’s next responder to my view controller:

let gameScene = GameScene(size: ...)
skView.presentScene(gameScene)

assert(gameScene.nextResponder == nil)
gameScene.nextResponder = self

Now my view controller receives keyboard events: problem solved! My guess is that SpriteKit’s designers intended for the keyboard event handling to be done in an SKScene subclass – that’s how it’s done in the Adventure sample code. Unfortunately that doesn’t make sense for my app architecture, where there’s more to the UI than an SKView.

Tags: ,