/ Tutorial

SpriteKit best practices

In this tutorial I'll show you some of the best practices that can help you to tackle the most annoying issues in SpriteKit.

Scene size

First of all, the size of a scene is always relative. It's pretty easy to fix this issue, you just have to set the scene size after the view did layout the subviews. With this trick the size of the scene will always match the size of your device screen.

NOTE: do NOT use the frame of the scene, use the size for coordinate geometry!

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    (self.view as? SKView)?.scene?.size = self.view.bounds.size
}

You can watch for changes inside your scene, there is a method for this purpose.

override func didChangeSize(_ oldSize: CGSize) {
    super.didChangeSize(oldSize)
    guard oldSize != self.size else { return }
    // do your stuff here
}

Scene rotation

If you want to have rotation support in your game, that's pretty easy too. You just have to implement one method inside your view controller, and delegate the size changes forward to your scene.

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    (self.view as? SKView)?.scene?.size = size
}

Pause, resume scene

In the past, this was a big issue in SpriteKit, because the scene always resumed itself after coming back from the background mode. Other issues happend as well, if you tried to pause the SKView. Fortunately nowadays these properties are working as it is expected, so no extra hacks like this one are needed. So remember, you can just set the isPaused property on the SKView and you are ready to go. Nice job Apple! ;)

Gradient SKTexture

Creating a gradient texture was also really painful, but now it's just one simple extension around the SKTexture class. You have to use a CILinearGradient filter, but be careful: you can NOT use this filter on a SKEffectNode, that will raise an exception and your app will crash. Anyway, here is the code:

import SpriteKit.SKTexture


extension SKTexture {
    
    enum GradientDirection {
        case up
        case left
        case upLeft
        case upRight
    }
    
    convenience init(size: CGSize, startColor: SKColor, endColor: SKColor, direction: GradientDirection = .up) {
        let context = CIContext(options: nil)
        let filter = CIFilter(name: "CILinearGradient")!
        let startVector: CIVector
        let endVector: CIVector
        
        filter.setDefaults()
        
        switch direction {
        case .up:
            startVector = CIVector(x: size.width/2, y: 0)
            endVector   = CIVector(x: size.width/2, y: size.height)
        case .left:
            startVector = CIVector(x: size.width, y: size.height/2)
            endVector   = CIVector(x: 0, y: size.height/2)
        case .upLeft:
            startVector = CIVector(x: size.width, y: 0)
            endVector   = CIVector(x: 0, y: size.height)
        case .upRight:
            startVector = CIVector(x: 0, y: 0)
            endVector   = CIVector(x: size.width, y: size.height)
        }
        
        filter.setValue(startVector, forKey: "inputPoint0")
        filter.setValue(endVector, forKey: "inputPoint1")
        filter.setValue(CIColor(color: startColor), forKey: "inputColor0")
        filter.setValue(CIColor(color: endColor), forKey: "inputColor1")
        
        let image = context.createCGImage(filter.outputImage!, from: CGRect(origin: .zero, size: size))
        
        self.init(cgImage: image!)
    }
}

Now you can use your linear gradient texture as usual, with any node objects.

SKNode zPosition

ALWAYS specify zPosition for every element in your scene.

Lesson learned, in the past I thought the z index is going to be calculated dynamically based on some kind of layers, or by the order of the addition of the node, but hell no. You should always set an explicit zPosition value (including SKShapeNodes) if you want to avoid some serious bug hunting in the future.

NOTE: You can find an example source code for this article on github.