How do you incorporate WKInterfaceSKScene into a watchOS Starter Template App?

I'm trying to create a Apple Watch game using Xcode 14.2 and watchOS 9. Getting started creating a watchOS App seems pretty straight forward, and getting started creating a game project via the starter template seems easy enough. Trying to put these two together though doesn't seem to work (or is not straight foward). The documentation specifies limitations on what libraries can be used with watchOS noting WKInterfaceSKScene, but doesn't give any specific examples of how to start out a WatchOS project using this. Additionally nearly every online tutorial that I'm able to find uses Storyboards to create a watchOS game, which does not seem to be supported in the latest version of watchOS or Xcode. Can anyone provide example starter code using the watchOS App project starter that that loads with a small colored square on the screen that moves from left to right using the WKInterfaceSKScene library?

I've tried the Apple documentation, asking ChatGPT for a sample or reference links, and various tutorials on YouTube and elsewhere.

Answered by acknapp in 746097022

the official Apple documentation here is a bit misleading. WKInterfaceSKScene is not needed, you can simple use SprikeKit for this. I was able to get a cyan ball to fall and bounce mid screen on the watch by modifying the Pong example from the watchOS with SwiftUI book. You can setup the same starter project for yourself by following the below steps:

  1. Start a watchOS App in XCode.
  2. Create a new swift file (which will be your game scene and put the code shown below in it).
  3. Modify the ContentView file by importing SpriteKit and SwiftUI and by modifying the body variable in the ContentView struct to look like the below.
  4. Build your project and it should show a cyan ball that falls and bounces mid screen.
var body: some View {
    GeometryReader { reader in
        SpriteView(scene: GameScene(size: reader.size))
            .focusable()
    }
}
import SpriteKit
import SwiftUI

final class GameScene: SKScene {
    private let cyan = UIColor(red: 0.0, green: 1.0, blue: 1.0, alpha: 1.0)
    private var ball: SKShapeNode!
    
    override init(size: CGSize) {
        super.init(size: size)
    }

    required init?(coder aDecoder: NSCoder) {
        // This was created via autofix
        fatalError("This should never be called")
    }

    override func update(_ currentTime: TimeInterval) {
        super.update(currentTime)
    }
}


extension GameScene {
    override func sceneDidLoad() {
        super.sceneDidLoad()
        ball = createBall()
        addChild(ball)

        let border = SKPhysicsBody(edgeLoopFrom: frame)
        border.friction = 0
        border.restitution = 1
        physicsBody = border
        physicsWorld.contactDelegate = self
    }
     
    func createBall() -> SKShapeNode {
        let node = SKShapeNode(circleOfRadius: 15)
        node.position = .init(x: frame.midX, y: frame.midY)
        node.fillColor = cyan            
        let body = SKPhysicsBody(circleOfRadius: 5)
        body.linearDamping = 0
        body.angularDamping = 0
        body.restitution = 1.0       
        node.physicsBody = body        
        return node
    }
}

extension GameScene: SKPhysicsContactDelegate {
    func didEnd(_ contact: SKPhysicsContact) {
        ball.physicsBody?.velocity = .zero
        ball.physicsBody?.applyImpulse(.init())
    }
}

extension SKPhysicsBody {
    func category(_ category: ShapeType, collidesWith: ShapeType, isDynamic: Bool = true) {
        categoryBitMask = category.rawValue
        collisionBitMask = collidesWith.rawValue
        contactTestBitMask = collidesWith.rawValue
        fieldBitMask = 0
        allowsRotation = false
        affectedByGravity = false
        friction = 0
        restitution = 0
        self.isDynamic = isDynamic
    }
}

struct ShapeType: OptionSet {
    let rawValue: UInt32
    static let ball = ShapeType(rawValue: 1 << 0)
}
Accepted Answer

the official Apple documentation here is a bit misleading. WKInterfaceSKScene is not needed, you can simple use SprikeKit for this. I was able to get a cyan ball to fall and bounce mid screen on the watch by modifying the Pong example from the watchOS with SwiftUI book. You can setup the same starter project for yourself by following the below steps:

  1. Start a watchOS App in XCode.
  2. Create a new swift file (which will be your game scene and put the code shown below in it).
  3. Modify the ContentView file by importing SpriteKit and SwiftUI and by modifying the body variable in the ContentView struct to look like the below.
  4. Build your project and it should show a cyan ball that falls and bounces mid screen.
var body: some View {
    GeometryReader { reader in
        SpriteView(scene: GameScene(size: reader.size))
            .focusable()
    }
}
import SpriteKit
import SwiftUI

final class GameScene: SKScene {
    private let cyan = UIColor(red: 0.0, green: 1.0, blue: 1.0, alpha: 1.0)
    private var ball: SKShapeNode!
    
    override init(size: CGSize) {
        super.init(size: size)
    }

    required init?(coder aDecoder: NSCoder) {
        // This was created via autofix
        fatalError("This should never be called")
    }

    override func update(_ currentTime: TimeInterval) {
        super.update(currentTime)
    }
}


extension GameScene {
    override func sceneDidLoad() {
        super.sceneDidLoad()
        ball = createBall()
        addChild(ball)

        let border = SKPhysicsBody(edgeLoopFrom: frame)
        border.friction = 0
        border.restitution = 1
        physicsBody = border
        physicsWorld.contactDelegate = self
    }
     
    func createBall() -> SKShapeNode {
        let node = SKShapeNode(circleOfRadius: 15)
        node.position = .init(x: frame.midX, y: frame.midY)
        node.fillColor = cyan            
        let body = SKPhysicsBody(circleOfRadius: 5)
        body.linearDamping = 0
        body.angularDamping = 0
        body.restitution = 1.0       
        node.physicsBody = body        
        return node
    }
}

extension GameScene: SKPhysicsContactDelegate {
    func didEnd(_ contact: SKPhysicsContact) {
        ball.physicsBody?.velocity = .zero
        ball.physicsBody?.applyImpulse(.init())
    }
}

extension SKPhysicsBody {
    func category(_ category: ShapeType, collidesWith: ShapeType, isDynamic: Bool = true) {
        categoryBitMask = category.rawValue
        collisionBitMask = collidesWith.rawValue
        contactTestBitMask = collidesWith.rawValue
        fieldBitMask = 0
        allowsRotation = false
        affectedByGravity = false
        friction = 0
        restitution = 0
        self.isDynamic = isDynamic
    }
}

struct ShapeType: OptionSet {
    let rawValue: UInt32
    static let ball = ShapeType(rawValue: 1 << 0)
}

For SwiftUI

Note: The touch location doesn't map 1:1 to the SKScene, so you have to convert the CGPoint based on the anchor of your SKScene and the size of your SKScene. Print the longPressLocation to find the coordinates.

import SwiftUI
import SpriteKit

struct ContentView : View {
    
    // MARK: - Properties -
    
    @State var gameScene : GameScene = GameScene(
        size: CGSize(
            width: GAME_WIDTH,
            height: GAME_HEIGHT
        )
    )
    
    // a long press gesture that enables isDragging
    
    @State var longPressLocation = CGPoint.zero

    // MARK: - Lifecycle -
    
    var body : some View {
        
        SpriteView(
            scene: gameScene
        )
        .ignoresSafeArea()
        .onAppear(
            perform: {
                
                // adjust the game scene
                
                gameScene.scaleMode = .aspectFit

            }
        )
        .gesture(
            LongPressGesture(
                minimumDuration: 0.0
            ).sequenced(
                before: DragGesture(
                    minimumDistance: 0.0,
                    coordinateSpace: .global
                )
            )
            .onEnded {
                value in
                switch value {
                    case .second(
                        true,
                        let drag
                    ):
                        
                        // touches ended

                        // capture location of touch
                        
                        longPressLocation = drag?.location ?? .zero   
                        
                        // find the SKSpriteNode

                        if let node : SKSpriteNode = gameScene.atPoint(
                            longPressLocation
                        ) as? SKSpriteNode {
                            
                            // do something with the SpriteKit node
                            
                        }

                    default:
                        break
                }
            }
        )

    }
    
}

#Preview {
    ContentView()
}
How do you incorporate WKInterfaceSKScene into a watchOS Starter Template App?
 
 
Q