The exceedingly strange case of SKView

Xcode 9.3; macOS 10.12.6

This came out of the strange case of the scrolling SpriteKit View, which did – in the end – work fine (as in, my SpriteKit scene was displaying) and failed when I tried to add interactions to it.

As a result, I went back to the drawing board, played some more with SpriteKit, and ended up with a simpler solution.

We build on Apple’s template, so I am not repeating the code here. The ViewController presents the SKScene; the connected GameScene.swift defines the interactions (mouseDown, mouseDragged, mouseUp, keyDown).

If you want to play along at home, remember to set each scene as the WindowController’s windowContent.

Case 1: Apple’s Game Template
ScreenShot2018-05-27at20.48.19-2018-05-27-20-46.pngScreenShot2018-05-27at21.07.42-2018-05-27-20-46.pngScreenShot2018-05-27at20.51.03-2018-05-27-20-46.pngPastedGraphic-2018-05-27-20-46.png

Controller: ViewController (presenting scene)
View Class: SKView
View Icon: NSView
View Attributes: NSView only
Connections: view, skView
Display: GameScene.sks (Hello World!)
Functionality: as defined in GameScene.swift (spinnyNode, fully interactive)
FPS: 60 (will occasionally drop to 57 or thereabouts)

Case 2: We implement the exact same settings, only instead of changing the class of the viewController’s view to ‘SKView’, we replace it with an SKView in Interface Builder. Note that the icon changes and that additional settings turn up in the ViewAttributes. We connect both view and skView outlet, and find that nothing happens.

ScreenShot2018-05-27at21.05.37-2018-05-27-20-46.pngScreenShot2018-05-27at21.07.14-2018-05-27-20-46.pngScreenShot2018-05-27at21.14.51-2018-05-27-20-46.pngScreenShot2018-05-27at20.58.51-2018-05-27-20-46.png

Controller: ViewController (presenting scene)
View Class: SKView
View Icon: SKView
View Attributes: SKView
Connections: view, skView
Display: black (denotes a working SKView)
Functionality: none
FPS: n/a

Case 3: We duplicate Case 2, and set the skView’s Scene to GameScene.sks

PastedGraphic1-2018-05-27-20-46.png

Controller: ViewController (presenting scene)
View Class: SKView
View Icon: SKView
View Attributes: SKView
Connections: view, skView, GameScene.sks
Display: GameScene.sks (Hello World!)
Functionality: as defined in GameScene.swift (spinnyNode, fully interactive)
FPS: 1.3 (this is not a typo. You can see it in the screenshot, and it is very, very slow indeed. Inacceptably slow.)

In other apps with a similar setup, this did not lead to any interaction, but quite frankly, ‘interaction’ at 1.3fps is almost indistinguishable from no interaction.

This was an incredibly frustrating setup. If you use the SKView provided by IB (which has the SpriteKit View settings exposed), it will not work without specifying the Scene, but it appears that the SceneEditor makes the scene incredibly slow.

Case 4: As in Case 3, but setting the ViewController’s class to the default NSViewController.

=ScreenShot2018-05-27at22.40.21-2018-05-27-20-46.png

Controller: NSViewController (not presenting scene)
View Class: SKView
View Icon: SKView
View Attributes: SKView
Connections: view, skView, GameScene.sks
Display: GameScene.sks (Hello World!) at large size
Functionality: none
FPS: n/a

This means that presenting a scene from a viewController using

if let scene = SKScene(fileNamed: “GameScene”) {
//…
view.presentScene(scene)
}

means that both the Scene.sks file (for providing the static elements) and the Scene.swift file (for providing the interaction) are used.

If you present a scene by setting the SKView’s scene to Scene.sks, only the static elements – with a different scaleMode – are displayed; all of the interaction goes missing.

Working hypothesis: there’s a conflict here.

Case 3a: Go back to Case 3, but comment out

view.presentScene(scene)

1__@__PastedGraphic-2018-05-27-20-46.png

Controller: ViewController (not presenting scene)
View Class: SKView
View Icon: SKView
View Attributes: SKView
Connections: view, skView, GameScene.sks
Display: GameScene.sks (Hello World!) at large size
Functionality: invisible (nodes get created but not displayed; key strokes are parsed correctly)
FPS: 1.3 fps

So now we’re retaining the connection between GameScene.sks and GameScene.swift, but we have the default presentation, and we retain the abysmal frame rate. What’s even more interesting is that the key presses are handled correctly, and clicking-and-dragging greatly increases the spinny node count, but no nodes are displayed. I assume this is because they have a limited lifespan. (A later experiment with less ephemeral nodes confirms this.)

Case 5: We’ll go through the whole thing again with the same ViewController and a new scene. Create Case5Scene.sks via File -> New, create Case5Scene.swift as a subclass of SKScene, and in the CustomClass Inspector of Case5Scene.sks, set its class as Case5Scene.swift.

Style it a bit – you want a brightly coloured background, a node, maybe a label and add a little interaction to Case5Scene.swift:
(my node is named ‘square’ in Case5Scene.sks)

var square: SKSpriteNode?
override func didMove(to view: SKView) {
square = self.childNode(withName: “square”) as? SKSpriteNode
}

override func mouseDown(with event: NSEvent) {
square?.run(SKAction.move(to: event.location(in: self), duration: 1.0))

let circle = SKShapeNode(circleOfRadius: 30)
circle.fillColor = .blue
circle.position = event.location(in: self)
self.addChild(circle)
}

Then duplicate Case 1 in the storyboard, set the new scene as the Window’s content controller, give it the name of Case 5 and change the relevant line in the ViewController to

if let scene = SKScene(fileNamed: “Case5Scene”) {

Build and Run: this scene behaves exactly as the previous iterations.

I do this as a safety step because it’s safest to change one step at a time: if things go wrong (for instance, you get no interaction), then you know which step was responsible. (Apple’s template does associate the .swift file with the .sks file; there is no automatic process of ‘create .sks, get associated .swift file free [or ‘subsclass SKScene, get checkbox for ‘also create SKS file’ as you get with nibs])

Case 6: Let’s duplicate Case 3 with our new scene and see what happens. Hypothesis: We get the glacial frame rate again. Reality: yup. Starting at 1fps and moving up to 1.3 fps.

Case 6a: What happens if we comment ‘present view’ out from the ViewController?
We retain the framerate which makes moving the square positively glacial, but now placing new nodes is also slowed down: the application feels sluggish overall.

And with that, I call it a wrap. We now know that the culprit in not being able to set up working SpriteKit scenes in IB is the inbuilt ‘SKView’ class, and that the way around this is to use a custom View (NSView), change its class to SKView, and use ‘presentScene’ in the view controller.

This means I can update and simplify the ScrollView tutorial, which in the first instance used an SKView created in code and went through a lot of ‘this doesn’t display anything/doesn’t allow interactions’. Since using sks files works (as long as you downcast an NSView), and sks files are lovely for laying out initial scenes, I will opt for using them in the future.