Munir Xavier Wanis

Build interactions using SpriteKit and SwiftUI

WWDC20 is blowing our minds this week with a lot of great new things and I'm particularly stuned with this new feature in SwiftUI: SpriteView. Now you can embed SpriteKit views in SwiftUI in a very simple way, you take your SKScene and pass it as an argument on SpriteView just like that:

import SwiftUI
import SpriteKit

class GameScene: SKScene {
    // Game code here
}

struct ContentView: View {
    var body: some View {
        SpriteView(scene: GameScene(size: CGSize(width: 300, height: 400)))
            .frame(width: 300, height: 400)
    }
}

It's recommended to use the same width/height as the GameScene, you can ignore it but have in mind that the scene will stretch to maintain its size.

With this feature we now have interesting possibilities, for example, we can have a game controller styled view below the scene that sends commands to the player in the scene.

I made a simple scene which the player is just a square 16 by 16 so we could move it with an old style keyboard arrow keys in the SwiftUI view. You can check the full code here, at my github repository to see the full thing, the code will only work on iOS 14+ and Xcode 12+, though.

Since the buttons will work to change the player's direction I made a simple enum called PlayerDirection that will contain five actions (up, down, left, right and stop) and a property called velocity that will return a CGVector containing the correct velocity for the player.

enum PlayerDirection: String {
    case up, down, left, right, stop
    
    var velocity: CGVector {
        switch self {
        case .up: return .init(dx: 0, dy: 100)
        case .down: return .init(dx: 0, dy: -100)
        case .left: return .init(dx: -100, dy: 0)
        case .right: return .init(dx: 100, dy: 0)
        case .stop: return .init(dx: 0, dy: 0)
        }
    }
}

Now we need to change our GameScene so it can receive the player velocity from outside, we will use the @Binding property so every time the user clicks on a button at ContentView it will automatically update the GameScene.

class GameScene: SKScene {
    // ...
    @Binding var currentDirection: PlayerDirection

    // ...
    init(_ direction: Binding<PlayerDirection>) {
        _currentDirection = direction
        super.init(size: CGSize(width: 414, height: 896)) // It's fixed to match scaled down iPhone resolutions for the sake of simplicity
        self.scaleMode = .fill
    }
    
    required init?(coder aDecoder: NSCoder) {
        _currentDirection = .constant(.up)
        super.init(coder: aDecoder)
    }
}

Great! We can now go to our ContentView and add the button logic! First we will add a @State property to the View containing a default value .stop so the player only starts moving when the user taps a button.

struct ContentView: View {
    @State private var currentDirection = PlayerDirection.stop
    
    var body: some View {
        Text("Hello World!")
    }
}

Now we will create a function that will take care of creating a button inside the View, we want to approach the arrow keys of old keyboards, just like that:

Arrow keys example
Good old arrow keys!

Below the body property add this:

func directionalButton(_ direction: PlayerDirection) -> some View {
    VStack {
        Button(action: { self.currentDirection = direction }, label: {
            Image(systemName: "arrow.\(direction.rawValue)")
                .frame(width: 50, height: 50, alignment: .center)
                .foregroundColor(.black)
                .background(Color.white.opacity(0.6))
        })
        .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
        .shadow(color: Color.black.opacity(0.2), radius: 10, x: 10, y: 10)
        .shadow(color: Color.white.opacity(0.7), radius: 10, x: -5, y: -5)
    }
}

For the View itself we will give the visual of an old gray computer, so let's put some LinearGradient for the background:

struct ContentView: View {
    @State private var currentDirection = PlayerDirection.stop
    
    var body: some View {
        ZStack {
            LinearGradient(gradient: Gradient(colors: [Color.gray, Color.gray.opacity(0.8), Color.gray]),
                           startPoint: .top,
                           endPoint: .bottom)
                .edgesIgnoringSafeArea(.all)
        }
    }
}
Background with Linear Gradient
Using linear gradients

Cool, we still have to add the GameScene and the arrow keys, so let's start with the scene first, put this below the LinearGradient:

VStack {
    SpriteView(scene: GameScene($currentDirection))
        .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) // Round all the views! :P
        .padding(50)
}
GameScene inside SwiftUI View
The result is the scene inside the SwiftUI View

Now we just need to add the arrow keys to the View so we can finally tap on the buttons and see the player moving! Add this code below the SpriteView:

VStack(spacing: 10) {
    HStack {
        // Add those spaces to force the button to be in the middle and conforms to the three buttons below
        Spacer()
        directionalButton(.up)
        Spacer()
    }
    HStack {
        directionalButton(.left)
        directionalButton(.down)
        directionalButton(.right)
    }
}
.padding(.all, 10)

And that's it! Here is the code in action:

Game in action
Nice! Now we just need to add more things so the game can look interesting! 😅

And that's it... This feature opens a lot of possibilities, imagine an adventure game that uses the inventory and action buttons as a SwiftUI view with all the cool animations and accessibility features way simpler to use.

The full code is available here: swiftui-spritekit, including the game logic so you can see how the velocity is implemented inside the scene.

Tagged with: