Using SpriteKit inside SwiftUI
Asked Answered
I

4

20

I am having an issue when creating a SpriteKit scene within SwiftUI. I created this project initially as a SwiftUI project.

Here is the code I have so far:

ContentView.swift:

/// Where the UI content from SwiftUI originates from.
struct ContentView : View {
    var body: some View {
        // Scene
        SceneView().edgesIgnoringSafeArea(.all)
    }
}

SceneView.swift:

/// Creates an SKView to contain the GameScene. This conforms to UIViewRepresentable, and so can be used within SwiftUI.
final class SceneView : SKView, UIViewRepresentable {
    
    // Conformance to UIViewRepresentable
    func makeUIView(context: Context) -> SKView {
        print("Make UIView")
        return SceneView(frame: UIScreen.main.bounds)
    }
    func updateUIView(_ uiView: SKView, context: Context) {
        print("Update UIView")
    }
    
    // Creating scene
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        let scene = Scene(size: UIScreen.main.bounds.size)
        presentScene(scene)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Scene.swift:

/// The scene for the game in SpriteKit.
final class Scene : SKScene {

    override func didMove(to view: SKView) {
        super.didMove(to: view)
    
        print("Scene didMove:")
    }
}

Problem

The problem is that the scene is reloading multiple times, as shown by the logs (because there are prints in the code):

Scene didMove:

Make UIView

Scene didMove:

Update UIView


As you can see, Scene didMove: is printed twice. I only want this to be called once, as I want to create my sprites here. Any ideas?

Isobelisocheim answered 16/6, 2019 at 0:28 Comment(1)
don't present your scene in the initSisely
C
12

iOS 14+

There is now a native view responsible for displaying a SKScene - it's called SpriteView.

Assuming we have a simple SKScene:

class Scene: SKScene {
    ...
}

we can use a SpriteView to display it directly in a SwiftUI view:

struct ContentView: View {
    var scene: SKScene {
        let scene = Scene()
        scene.size = CGSize(width: 300, height: 400)
        scene.scaleMode = .fill
        return scene
    }

    var body: some View {
        SpriteView(scene: scene)
            .frame(width: 300, height: 400)
            .edgesIgnoringSafeArea(.all)
    }
}

You can find more information here:

Cauchy answered 11/8, 2020 at 22:9 Comment(1)
I belive the question was about iOS 13. SpriteView is available since 14.Seasickness
B
4

Here's a SpriteKit container View which can be used this way:

SpriteKitContainer(sceneName: "MainScene")

struct SpriteKitContainer : UIViewRepresentable {

    let sceneName: String

    class Coordinator: NSObject {
        var scene: SKScene?
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }

    func makeUIView(context: Context) -> SKView {
        let view = SKView(frame: .zero)
        view.preferredFramesPerSecond = 60
        view.showsFPS = true
        view.showsNodeCount = true

       //load SpriteKit Scene
       guard let aScene = SKScene(fileNamed: sceneName)
       else {
            view.backgroundColor = UIColor.red
            return view
       }
       aScene.scaleMode = .resizeFill
       context.coordinator.scene = aScene
       return view
    }


    func updateUIView(_ view: SKView, context: Context) {
       view.presentScene(context.coordinator.scene)
    }

}
#if DEBUG
struct ContentView_Previews : PreviewProvider {

   static var previews: some View {

      // Replace "MainScene" with your SpriteKit scene file name
      SpriteKitContainer(sceneName: "MainScene")
         .edgesIgnoringSafeArea(.all)
         .previewLayout(.sizeThatFits)
      }
}
#endif
Beshrew answered 19/7, 2019 at 20:20 Comment(1)
I used the above approach to add a physics-driven animation to my SwiftUI app. The view was on a detail view for a list and I found going back from the detail view left the animation running in the background. The only way I found to solve this was keeping an animation pause state on the detail view and changing that with the onAppear and onDisappear for my UIViewRepresentable containing the SKScene, which in turn pauses the scene. Hopefully there is, or will be, a better way. Check you're not creating a battery killer with this SpriteKit-SwiftUI integration though!Halophyte
T
1

This is how i solved it:

ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        SpriteKitContainer(scene: SpriteScene())
    }
}

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

SpriteKitContainer.swift

import SwiftUI
import SpriteKit

struct SpriteKitContainer: UIViewRepresentable {
    typealias UIViewType = SKView
    
    var skScene: SKScene!
    
    init(scene: SKScene) {
        skScene = scene
        self.skScene.scaleMode = .resizeFill
    }
    
    class Coordinator: NSObject {
        var scene: SKScene?
    }
    
    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        coordinator.scene = self.skScene
        return coordinator
    }
    
    func makeUIView(context: Context) -> SKView {
        let view = SKView(frame: .zero)
        view.preferredFramesPerSecond = 60
        view.showsFPS = true
        view.showsNodeCount = true
        
        return view
    }
    
    func updateUIView(_ view: SKView, context: Context) {
        view.presentScene(context.coordinator.scene)
    }
}

struct SpriteKitContainer_Previews: PreviewProvider {
    static var previews: some View {
        Text("Hello, World!")
    }

}

SpriteKitScene.swift

import UIKit
import SpriteKit

class SpriteScene: SKScene {
    
    //change the code below to whatever you want to happen on skscene
    
    override func didMove(to view: SKView) {
        physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        
        let location = touch.location(in: self)
        let box = SKSpriteNode(color: .red, size: CGSize(width: 50, height: 50))
        box.position = location
        box.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 50, height: 50))
        addChild(box)
    }
}

PS: you'll only see the spritekitscene working in the simulator, it won't work in the preview

Taegu answered 16/9, 2020 at 13:33 Comment(0)
E
0

A Coordinator is not necessary to present a scene; the Context is sufficient. Here is what I use to load a Scene.sks file:

struct ContentView : View {
    var body: some View {

        SKViewContainer()

    }
}

struct SKViewContainer: UIViewRepresentable {

    func makeUIView(context: Context) -> SKView {

        let view = SKView() 

        guard let scene = SKScene(fileNamed: "Scene")
            else {
                view.backgroundColor = UIColor.red
                return view
        }

        view.presentScene(scene)

        return view    
    }

    func updateUIView(_ uiView: SKView, context: Context) {}

}
Enfilade answered 23/4, 2020 at 19:49 Comment(1)
From UIViewRepresentable doc:"The system doesn’t automatically communicate changes occurring within your view to other parts of your SwiftUI interface. When you want your view to coordinate with other SwiftUI views, you must provide a Coordinator instance to facilitate those interactions. For example, you use a coordinator to forward target-action and delegate messages from your view to any SwiftUI views."Fragment

© 2022 - 2024 — McMap. All rights reserved.