SpriteView doesn't pause scene on state change

Problem

It seems that SpriteView doesn't pause the SKScene with a state property passed to the SpriteView(scene:isPaused:) initializer.

Sample Project

I created a sample Xcode 13 project on GitHub running on an iOS 15 simulator with a ContentView with a @State var paused property that is sent to a child SpriteView(scene: scene, isPaused: paused). The state property is changed by a "Paused:" button. The property changes and the text on the button is updated, but the scene is never paused by the SpriteView.

It looks like the SpriteView is not picking up the updates and is not pausing the underlying SKView and SKScene inside it.

https://i.stack.imgur.com/Qt0qY.gif

All the relevant code is in ContentView.swift:

import SwiftUI
import SpriteKit

class GameScene: SKScene, ObservableObject {
    @Published var updates = 0
    private let label = SKLabelNode(text: "Updates in SKScene:\n0")
    
    override func didMove(to view: SKView) {
        addChild(label)
        label.numberOfLines = 2
    }
    
    override func update(_ currentTime: TimeInterval) {
        updates += 1
        label.text = "Updates in SKScene:\n\(updates)"
    }
}

struct ContentView: View {
    @State private var paused = false
    
    @StateObject private 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 {
        if #available(iOS 15.0, *) {
            print(Self._printChanges())
        }
        return ZStack {
            SpriteView(scene: scene, isPaused: paused).ignoresSafeArea()
            VStack {
                Text("Updates from SKScene: \(scene.updates)").padding().foregroundColor(.white)
                Button("Paused: \(paused)" as String) {
                    paused.toggle()
                }.padding()
                Spacer()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Restart Problem

I also created a second GitHub project that is similar to the first one but it also has a "Restart" button that shows the same problem.

The "Restart" button should recreate the SKScene when pressed. The SKScene gets recreated inside the SceneStore and the ContentView's Text view (with a new time assigned to the name property of the scene), but the SpriteView doesn't change the scene.

It seems that the SpriteView keeps the initial scene in memory, and doesn't let it go to replace it with the new scene. This can be seen in the Console, by hitting the Restart button twice and looking for something like "-- Scene 7:49:44 PM deinit --".

The updates from the @Published var updates = 0 property inside the scene also stop (at the top of the screen), because the new scene that gets created is not added into the view, so the SKScene.didMove(to view:) method is never called.

https://i.stack.imgur.com/gGPLp.gif

The relevant code for this one is in ContentView.swift:

import SwiftUI
import SpriteKit

class GameScene: SKScene, ObservableObject {
    @Published var updates = 0
    private let label = SKLabelNode(text: "Updates in SKScene:\n0")
    
    override func didMove(to view: SKView) {
        addChild(label)
        label.numberOfLines = 4
        label.position = CGPoint(x: 0, y: -100)
    }
    
    override func update(_ currentTime: TimeInterval) {
        updates += 1
        label.text = "Updates in SKScene:\n\(updates)\nScene created at:\n\(name!)"
    }
    
    deinit {
        print("-- Scene \(name!) deinit --")
    }
}

class SceneStore : ObservableObject {
    @Published var currentScene: GameScene
    
    init() {
        currentScene = SceneStore.createScene()
    }
    
    func restartLevel() {
        currentScene = SceneStore.createScene()
    }
    
    // MARK: - Class Functions
    
    static private func createScene() -> GameScene {
        let scene = GameScene()
        scene.size = CGSize(width: 300, height: 400)
        scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        scene.scaleMode = .fill
        scene.name = Date().formatted(date: .omitted, time: .standard)
        return scene
    }
}

struct ContentView: View {
    @EnvironmentObject private var sceneStore: SceneStore
    @EnvironmentObject private var scene: GameScene
    @State private var paused = false
    
    var body: some View {
        if #available(iOS 15.0, *) {
            print(Self._printChanges())
        }
        return ZStack {
            SpriteView(scene: scene, isPaused: paused).ignoresSafeArea()
            VStack {
                Text("Updates from SKScene: \(scene.updates)").padding().foregroundColor(.white)
                Text("Scene created at: \(scene.name!)" as String).foregroundColor(.white)
                Button("Restart") {
                    sceneStore.restartLevel()
                }.padding()
                Button("Paused: \(paused)" as String) {
                    paused.toggle()
                }
                Spacer()
            }
        }
    }
}

Question / Workaround?

Am I missing something? Or is this a bug? If so, is there any workaround?

Answered by calin in 691699022

The solution is to use an id on the GameScene and send it to the SpriteView with the .id() modifier, to force SwiftUI to create a new SpriteView.

class GameScene: SKScene, ObservableObject {
    var id = UUID()
}
SpriteView(scene: scene, isPaused: paused).id(scene.id)

This question was answered on SO: https://stackoverflow.com/questions/69610165/spriteview-doesnt-pause-scene-on-state-change/69610906#69610906 by @jnpdx.

Accepted Answer

The solution is to use an id on the GameScene and send it to the SpriteView with the .id() modifier, to force SwiftUI to create a new SpriteView.

class GameScene: SKScene, ObservableObject {
    var id = UUID()
}
SpriteView(scene: scene, isPaused: paused).id(scene.id)

This question was answered on SO: https://stackoverflow.com/questions/69610165/spriteview-doesnt-pause-scene-on-state-change/69610906#69610906 by @jnpdx.

SpriteView doesn't pause scene on state change
 
 
Q