How to transitions between scenes using SpriteView in SwiftUI?

116 views Asked by At

I have two scenes I want to transition between:

class TestGameScene: SKScene {
    override func didMove(to view: SKView) {
        size = view.frame.size
        scaleMode = .resizeFill
        let textNode = SKLabelNode(text: "Game Scene")
        textNode.position.x = size.width / 2
        textNode.position.y = size.height / 2
        addChild(textNode)
    }
}

to

class TestGameOverScene: SKScene {
    override func didMove(to view: SKView) {
        size = view.frame.size
        scaleMode = .resizeFill
        backgroundColor = .red
        let textNode = SKLabelNode(text: "Game Over")
        textNode.position.x = size.width / 2
        textNode.position.y = size.height / 2
        addChild(textNode)
    }
}

I am currently presenting TestGameScene via a SpriteView:

struct ContentView: View {
    var body: some View {
        SpriteView(scene: TestGameScene())
    }
}

In order to transition to the Game Over scene, I overrided touchesBegan(_:with:) in TestGameScene to present the Game Over scene like so:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    view!.presentScene(TestGameOverScene(), transition: .fade(withDuration: 1.0))
}

Although this works, the TestGameOverScene gets deinitialised if the app goes into the background, which causes the Game Over scene to disappear. Is there a way to prevent this from happening or a better way to transition between two scenes when using SpriteView? I would like to use the fancy transitions provided by SpriteKit eg. .doorsOpenVertical(withDuration: 0.5) etc etc.

1

There are 1 answers

0
rayaantaneja On

After messing around with a few ideas I came up with solution that works, however, I would love some criticism / analysis of how good this method is as I don't know much about memory management.

This class provides ContentView with the scene to render using SpriteView. It's a published property as when it changes, SwiftUI should be notified.

final class SceneProvider: ObservableObject { 
    @Published var scene: SKScene

    init() { 
        let initialScene = TestGameScene()
        self.scene = initialScene
        initialScene.viewState = self
    }
}

Added a binding/reference to the scene being presented by SwiftUI. We'll update this reference to the newly presented scene to notify SwiftUI.

class TestGameScene: SKScene { 
    unowned var viewState: SceneProvider!
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let sceneToPresent = TestGameOverScene()
        viewState.scene = sceneToPresent  // << we do this so SwiftUI updates
        view!.presentScene(sceneToPresent, transition: .fade(withDuration: 1.0))
    }
    // .. Rest is the same
}

From here, I could perhaps create a custom SKScene subclass that encapsulates all of this behaviour.

And of course,

struct ContentView: View { 
    @StateObject var sceneProvider = SceneProvider()  // << @StateObject ensures the container for the presented scene, ie. SceneProvider, isn't deallocated
    
    var body: some View { 
        SpriteView(scene: sceneProvider.scene)
    }
}

Using this method, TestGameScene gets deinitialised when the TestGameOverScene is presented - which is good (I think). In addition to this, SwiftUI maintains the newly presented scene even if you go to the background and come back as the @Published property along with @StateObject keeps SwiftUI up to date.

As there is only one scene held by SceneProvider, we don't have multiple scenes just taking up space in memory - which is good again (this I know is good for sure).

Would love some feedback on this.