1
votes

Goal: SwiftUI toggle button that controls a SwiftUI View with Scenekit (UIViewRepresentable)

// show statistics such as fps and timing information

What I did: This is the UIViewRepresentable ScenekitView

import SwiftUI
import SceneKit

struct ScenekitView : UIViewRepresentable {
    let scene = SCNScene(named: "art.scnassets/ship.scn")!

    func makeUIView(context: Context) -> SCNView {
        // create and add a camera to the scene
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)

        // place the camera
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)

        // create and add a light to the scene
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light!.type = .omni
        lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
        scene.rootNode.addChildNode(lightNode)

        // create and add an ambient light to the scene
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light!.type = .ambient
        ambientLightNode.light!.color = UIColor.darkGray
        scene.rootNode.addChildNode(ambientLightNode)

        // retrieve the ship node
        let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!

        // animate the 3d object
        ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))

        // retrieve the SCNView
        let scnView = SCNView()
        return scnView
    }

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

        // allows the user to manipulate the camera
        scnView.allowsCameraControl = true

        // show statistics such as fps and timing information

        scnView.showsStatistics = true

        // configure the view
        scnView.backgroundColor = UIColor.black

 }

}
struct ScenekitView_Previews: PreviewProvider {
    static var previews: some View {
        ScenekitView()
    }
}

This is the Menu to control Scenekit

import SwiftUI


struct Menu: View {

 @State var showstats = false


    var body: some View {

        HStack {

        Form {
            Toggle(isOn:) {
               Text("Stats")
            }
        }
        .frame(width: 200.0)

            Spacer()
    }
  }
 }
struct Menu_Previews: PreviewProvider {
    static var previews: some View {
        Menu()
    }
}

And this is Main Scene, with Menu on top of Scenekit:

import SwiftUI

struct MainView: View {
    var body: some View {

        ZStack {
        ScenekitView()
        Menu()
        }


    }
}

Question: How can I pass data from ScenekitView to Menu, so I can toggle and show/hide stats?

I tried ObservableObject but could not make it work. Also looked into other SO threads, but none worked for me.

Any help is much appreciated!

Cheers to allenter image description here

1
What you are looking for is a Coordinator like shown in Apples SwiftUI Tutorial: developer.apple.com/tutorials/swiftui/interfacing-with-uikitKevinP
You should not, instead create view model (as ObservableObject) an pass it and in menu and in your scene, so modifying that view model from any place both view could react correspondingly.Asperi

1 Answers

4
votes

Updated: March 11, 2020.

To pass data to updateNSView(_:context:) or updateUIView(_:context:) instance method you need to create @State and @Binding properties.

Use the following code to find out how to do it:

I've written this code for macOS but you can easily change it for iOS:

import SwiftUI
import SceneKit

struct ContentView: View {

    @State var stats: Bool = true

    var body: some View {
        HStack {
            ScenekitView(showStats: $stats)
            VStack {
                Button(action: {
                    self.stats.toggle()
                }, label: {
                    Text(self.stats ? "ON" : "OFF")
                })
            }
            .frame(width: 150.0)
        }
    }
}

struct ScenekitView: NSViewRepresentable {

    @Binding var showStats: Bool
    let sceneView = SCNView(frame: .zero)
    let scene = SCNScene(named: "art.scnassets/ship.scn")!

    func scnScene(stat: Bool, context: Context) -> SCNView {
        sceneView.scene = scene
        sceneView.showsStatistics = stat
        return sceneView
    }

    func makeNSView(context: Context) -> SCNView {
        scnScene(stat: true, context: context)
    }

    func updateNSView(_ uiView: SCNView, context: Context) {
        uiView.allowsCameraControl = true
        uiView.backgroundColor = NSColor.black
        uiView.showsStatistics = showStats        
    }
}

enter image description here

enter image description here

Additionally you can use Coordinator object and delegate to this coordinator for switching objects using control property and for updating anything at 60 fps inside renderer() method (for example).

In this case you do not need to use updateNSView() or updateUIView() method:

struct ScenekitView: NSViewRepresentable {

    @Binding var showStats: Bool
    let sceneView = SCNView(frame: .zero)
    let scene = SCNScene(named: "art.scnassets/ship.scn")!

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

    final class Coordinator: NSObject, SCNSceneRendererDelegate {
        var control: ScenekitView

        init(_ control: ScenekitView) {
            self.control = control
        }

        func renderer(_ renderer: SCNSceneRenderer,
               updateAtTime time: TimeInterval) {

            control.sceneView.showsStatistics = control.showStats

            for i in 0...255 {
                control.sceneView.backgroundColor = NSColor(
                                  red: CGFloat(arc4random_uniform(UInt32(i))),
                                green: CGFloat(arc4random_uniform(UInt32(i))),
                                 blue: CGFloat(arc4random_uniform(UInt32(i))),
                                alpha: 1.0)
            }
        }
    }

    func scnScene(stat: Bool, context: Context) -> SCNView {
        sceneView.scene = scene
        sceneView.showsStatistics = stat
        sceneView.delegate = context.coordinator
        return sceneView
    }

    func makeNSView(context: Context) -> SCNView {
        scnScene(stat: true, context: context)
    }

    func updateNSView(_ uiView: SCNView, context: Context) { }
}