How can you pause a SpriteKit scene from SwiftUI, without reinitializing it every time?

I am trying to use the isPaused: argument in the SpriteView initializer to pause the SKScene when a state property changes in SwiftUI.

@State private var showingLevelChooser = false

var body: some View {
    SpriteView(scene: scene, isPaused: showingLevelChooser)
}

But when I use the state variable like:

SpriteView(scene: scene, isPaused: showingLevelChooser)

instead of just:

SpriteView(scene: scene)

the SKScene is recreated every time the variable changes, which is not what I want. I only want to pause the game.

As far as I understand this happens because SwiftUI recreates the views that are dependent on the state.

But if this is the case, how can you pause the SpriteKit scene from SwiftUI, without reinitializing it every time?

I created a sample Xcode 13 project here: https://github.com/clns/SpriteView-isPaused

The SKScene is displaying the time elapsed on the screen, in seconds. Every time the SwiftUI sheet is presented and the state variable changes, the timer starts from 0 (zero), because the scene is recreated.

A preview is included in the GitHub project. I cannot upload an animated gif here.

Answered by OOPer in 690174022

You should better include the code as text, which helps involving more readers.


In your code, scene is a computed property, so it is evaluated at each time body is evaluated.

Generally, you should better avoid creating a computed property which creates a new instance, especially when the identity is important.

Please try something like this:

class GameScene: SKScene, ObservableObject { //<-
    private let label = SKLabelNode(text: "Time Elapsed:\n0")
    private var lastUpdateTime : TimeInterval = 0
    
    override func didMove(to view: SKView) {
        addChild(label)
    }
    
    override func update(_ currentTime: TimeInterval) {
        if (self.lastUpdateTime == 0) {
            self.lastUpdateTime = currentTime
        }
        
        let seconds = Int(currentTime - lastUpdateTime)
        label.text = "Time Elapsed:\n\(seconds)"
        label.numberOfLines = 2
    }
}

struct ContentView: View {
    @State private var showingLevelChooser = false
    
    //↓
    @StateObject var scene: GameScene = {
        let scene = GameScene()
        scene.size = CGSize(width: 300, height: 400)
        scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        scene.scaleMode = .fill
        return scene
    }()
    
    var body: some View {
        ZStack {
            SpriteView(scene: scene, isPaused: showingLevelChooser)
                .ignoresSafeArea()
            VStack {
                Button("Level Chooser") {
                    showingLevelChooser.toggle()
                }
                Spacer()
            }
        }
        .sheet(isPresented: $showingLevelChooser) {
            VStack {
                Button("Cancel") {
                    showingLevelChooser.toggle()
                }
                Text("Level Chooser")
            }
        }
    }
}
Accepted Answer

You should better include the code as text, which helps involving more readers.


In your code, scene is a computed property, so it is evaluated at each time body is evaluated.

Generally, you should better avoid creating a computed property which creates a new instance, especially when the identity is important.

Please try something like this:

class GameScene: SKScene, ObservableObject { //<-
    private let label = SKLabelNode(text: "Time Elapsed:\n0")
    private var lastUpdateTime : TimeInterval = 0
    
    override func didMove(to view: SKView) {
        addChild(label)
    }
    
    override func update(_ currentTime: TimeInterval) {
        if (self.lastUpdateTime == 0) {
            self.lastUpdateTime = currentTime
        }
        
        let seconds = Int(currentTime - lastUpdateTime)
        label.text = "Time Elapsed:\n\(seconds)"
        label.numberOfLines = 2
    }
}

struct ContentView: View {
    @State private var showingLevelChooser = false
    
    //↓
    @StateObject var scene: GameScene = {
        let scene = GameScene()
        scene.size = CGSize(width: 300, height: 400)
        scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        scene.scaleMode = .fill
        return scene
    }()
    
    var body: some View {
        ZStack {
            SpriteView(scene: scene, isPaused: showingLevelChooser)
                .ignoresSafeArea()
            VStack {
                Button("Level Chooser") {
                    showingLevelChooser.toggle()
                }
                Spacer()
            }
        }
        .sheet(isPresented: $showingLevelChooser) {
            VStack {
                Button("Cancel") {
                    showingLevelChooser.toggle()
                }
                Text("Level Chooser")
            }
        }
    }
}

Thanks a lot @OOPer. It works as intended.

One more related question. To track the paused state inside of the SpriteKit scene, I guess the best option would be to call a method on the scene, maybe in the onChange(of:perform:) modifier?

var body: some View {
    ZStack {
      SpriteView(scene: scene, isPaused: showingLevelChooser)
        .ignoresSafeArea()
      VStack {
        Button("Level Chooser") {
          showingLevelChooser.toggle()
        }
        Spacer()
      }
    }
    .sheet(isPresented: $showingLevelChooser) {
      VStack {
        Button("Cancel") {
          showingLevelChooser.toggle()
        }
        Text("Level Chooser")
      }
      .background(BackgroundClearView())
    }
    //↓
    .onChange(of: showingLevelChooser) { value in
      scene.isPaused(value)
    }
  }
How can you pause a SpriteKit scene from SwiftUI, without reinitializing it every time?
 
 
Q