Search code examples
swiftswiftuisprite-kitobservation

Accessing a SpriteKit method from SwiftUI


I want to call a method defined inside a SpriteKit class with a SwiftUI button. I am using the @Observable macro on the class, and @State for the class instance inside SwiftUI.

Some of the class properties are accessible, but not the method I want to use. Calling it throws 3 compiler errors:

  • Cannot call value of non-function type 'Binding'
  • Referencing subscript 'subscript(dynamicMember:)' requires wrapper 'Binding'
  • Value of type 'SKScene' has no dynamic member 'resetToInitialConditions' using key path from root type 'SKScene'

Using @Bindable instead of @State doesn't solve the issue.

What am I missing?

SwiftUI code:

import SwiftUI
import SpriteKit

struct ActionReplay: View {
    @State var myActionScene: SKScene = ActionScene(size: CGSize(width: 1000, height: 1000))
    
    var body: some View {
        ZStack {
            SpriteView(scene: myActionScene, debugOptions: [.showsFPS, .showsNodeCount])
                .ignoresSafeArea()
            VStack {
                Spacer()
                HStack {
                    Button(action: {
                        myActionScene.resetToInitialConditions() // Errors here
                    }, label: {
                        Text("Reset")
                    })
                    .buttonStyle(.bordered)
                }
            }
        }
    }
}

Data and SpriteKit:

struct InitialConditions {
    var position: CGPoint = .zero
    var rotation: CGFloat = 5
    var velocity: CGVector = CGVector(dx: 0, dy: 0)
    var angularVelocity: CGFloat = 0
}

@Observable class ActionScene: SKScene {
    
    var initialConditions = InitialConditions()
    var baseObject: SKSpriteNode!
    var ground: SKSpriteNode!
    
    override init(size: CGSize) {
        super.init(size: size)
        self.scaleMode = .resizeFill
        self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
    }
    
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func didMove(to view: SKView) {
        backgroundColor = .white
        createObjects()
    }
    
    func createObjects() {
        baseObject = SKSpriteNode(color: .red, size: CGSize(width: 60, height: 60))
        baseObject.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        baseObject.position = initialConditions.position
        baseObject.zRotation = initialConditions.rotation
        baseObject.physicsBody = SKPhysicsBody(rectangleOf: baseObject.size)
        baseObject.physicsBody?.restitution = 1.1
        baseObject.physicsBody?.linearDamping = 0
        addChild(baseObject)
        
        ground = SKSpriteNode(color: .darkGray, size: CGSize(width: 370, height: 10))
        ground.position = CGPoint(x: .zero, y: -300)
        ground.physicsBody = SKPhysicsBody(rectangleOf: ground.size)
        ground.physicsBody?.affectedByGravity = false
        ground.physicsBody?.isDynamic = false
        addChild(ground)
    }
    
    func resetToInitialConditions() {
        baseObject.physicsBody?.velocity = initialConditions.velocity
        baseObject.physicsBody?.angularVelocity = initialConditions.angularVelocity
        baseObject.position = initialConditions.position
        baseObject.zRotation = initialConditions.rotation
    }
    
    // Testing the method I want to call
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        resetToInitialConditions()
    }
}

Solution

  • You've defined your @State as:

    @State var myActionScene: SKScene = ActionScene(size: CGSize(width: 1000, height: 1000))
    

    This is telling the compiler that myActionScene is a SKScene. But, in reality, you have a subclass of SKScene, called ActionScene.

    Because you're telling the compiler that it's really just a SKScene (which, technically it is -- that is the superclass of ActionScene), it thinks that the only methods/properties available are those defined on SKScene. If you want access to what is defined on ActionScene, you must tell the compiler that's what it is:

    @State var myActionScene: ActionScene = ActionScene(size: CGSize(width: 1000, height: 1000))
    

    Or, the compiler will even infer the type for you:

    @State var myActionScene = ActionScene(size: CGSize(width: 1000, height: 1000))