Search code examples
iosswiftuiscenekitscnaction

Perform SCNAction using a button (SwiftUI)


With UIKit I have created a SceneView with an object, that can perform SCNActions when a button has been pressed:

import UIKit
import SceneKit

class ViewController: UIViewController {


    @IBOutlet weak var ScenekitView: SCNView!
    
    
    var scene:SCNScene!
    var ship:SCNNode!
    
    
    @IBAction func button(_ sender: Any) {
        let sequence = SCNAction.sequence([SCNAction.moveBy(x: 0, y: 0, z: -10, duration: 0.5),SCNAction.moveBy(x: 0, y: 0, z: 10, duration: 0.5)])
        ship.runAction(sequence)
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        scene = SCNScene(named: "mainScene.scn")!
        
        var cameraNode: SCNNode!
        cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
        cameraNode.scale = SCNVector3(1, 1, 0.5)
        cameraNode.eulerAngles = SCNVector3(0, 0, 0)

        ScenekitView.scene = scene
        
        ship = scene.rootNode.childNode(withName: "ship reference", recursively: true)!
        let pos = SCNVector3Make(0, 0, 0)
        
        ship.runAction(SCNAction.move(to: pos, duration: 1))
    }
}

Now I want to achieve the same using SwiftUI.

While I am able to, in this example, let the camera perform an action once the view has loaded, I have no idea how to let it perform another action after the press of a button. Especially because the node is defined in a function in another sub view.

This is what I got so far:

import SwiftUI
import SceneKit


struct ContentView: View { 
    var body: some View{

        VStack {
            ScenekitView().ignoresSafeArea()

            Button("move") {
               //here I would like to call a function including a SCNAction

            }
        }   
    }
}


struct ScenekitView : UIViewRepresentable {

    func makeUIView(context: Context) -> SCNView {

        let scene = SCNScene(named: "SceneFile.scn")!

        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
        scene.rootNode.addChildNode(cameraNode)
        
        cameraNode.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 5, duration: 1)))

        let scnView = SCNView()
        return scnView
    }

    func updateUIView(_ scnView: SCNView, context: Context) {
        scnView.scene = scene
        scnView.allowsCameraControl = false
    }
}

Thanks for any helpful ideas.


Solution

  • You can create an object owned by the parent view (ContentView) that distributes the scene reference to the ScenekitView and allows access from the Button.

    You might want to adjust details (like where the camera setup is done), but this is the general concept:

    class SceneManager : ObservableObject {
        let scene = SCNScene(named: "SceneFile.scn")!
        var ship:SCNNode
        
        init() {
            ship = scene.rootNode.childNode(withName: "ship reference", recursively: true)!
        }
        
        func move() {
            let pos = SCNVector3Make(0, 0, 0)
            ship.runAction(SCNAction.move(to: pos, duration: 1))
        }
    }
    
    struct ContentView: View {
        @StateObject var sceneManager = SceneManager()
        
        var body: some View{
    
            VStack {
                ScenekitView(scene: sceneManager.scene)
                    .ignoresSafeArea()
    
                Button("move") {
                    sceneManager.move()
                }
            }
        }
    }
    
    
    struct ScenekitView : UIViewRepresentable {
        var scene : SCNScene
        
        func makeUIView(context: Context) -> SCNView {
            let cameraNode = SCNNode()
            cameraNode.camera = SCNCamera()
            cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
            scene.rootNode.addChildNode(cameraNode)
            
            cameraNode.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 5, duration: 1)))
            let scnView = SCNView()
            scnView.scene = scene
            return scnView
        }
    
        func updateUIView(_ scnView: SCNView, context: Context) {
            scnView.allowsCameraControl = false
        }
    }