I’m trying to implement a very basic SwiftUI app with an underlying SceneKit scene/view. The buttons in the SwiftUI view should manipulate the content of the scene and vice versa the content of the scene should determine if the buttons are active or inactive.
Yes, I’ve read and watched the Apple developer sessions 226: Data Flow through SwiftUI and 231: Integrating SwiftUI. And I worked my way through the Apple tutorials. But I’m little lost here. Any hints and directions are appreciated.
Here is the code:
I have a MainView, which uses a SceneKit SCNView
with a HUDView
on top:
import SwiftUI
struct MainView: View {
var body: some View {
ZStack {
SceneView()
HUDView()
}
}
}
The SceneView
integrates a SCNView
via the UIViewRepresentable
protocol. The scene has two functions to add and remove the box from the scene:
import SwiftUI
import SceneKit
struct SceneView: UIViewRepresentable {
let scene = SCNScene()
func makeUIView(context: Context) -> SCNView {
// create a box
scene.rootNode.addChildNode(createBox())
// code for creating the camera and setting up lights is omitted for simplicity
// …
// retrieve the SCNView
let scnView = SCNView()
scnView.scene = scene
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// configure the view
scnView.backgroundColor = UIColor.gray
// show statistics such as fps and timing information
scnView.showsStatistics = true
scnView.debugOptions = .showWireframe
}
func createBox() -> SCNNode {
let boxGeometry = SCNBox(width: 20, height: 24, length: 40, chamferRadius: 0)
let box = SCNNode(geometry: boxGeometry)
box.name = "box"
return box
}
func removeBox() {
// check if box exists and remove it from the scene
guard let box = scene.rootNode.childNode(withName: "box", recursively: true) else { return }
box.removeFromParentNode()
}
func addBox() {
// check if box is already present, no need to add one
if scene.rootNode.childNode(withName: "box", recursively: true) != nil {
return
}
scene.rootNode.addChildNode(createBox())
}
}
The HUDView
bundles the buttons with actions to add and remove the box from the underlying scene. If the box object is in the scene, the add button should be inactive, only the remove button should be active:
struct HUDView: View {
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .center, spacing: 2) {
Spacer()
ButtonView(action: {}, icon: "plus.square.fill", isActive: false)
ButtonView(action: {}, icon: "minus.square.fill")
ButtonView(action: {}) // just a dummy button
}
.background(Color.white.opacity(0.2))
Spacer()
}
}
}
The buttons are fairly simple as well, they take an action and optionally an icon as well as their initial active/inactive state:
struct ButtonView: View {
let action: () -> Void
var icon: String = "square"
@State var isActive: Bool = true
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.title)
.accentColor(isActive ? Color.white : Color.white.opacity(0.5))
}
.frame(width: 44, height: 44)
}
}
The resulting app is pretty simple:
The SceneKit scene is rendered correctly and I am able to manipulate the camera on screen.
But how do I connect the button actions to the corresponding scene functions? How do I let the scene inform the HUDView about it’s content (box is present or not), so it can set the active/inactive state of the buttons?
What is the best approach to this task? Should I implement a Coordinator
in SceneView
? Should I use a separate SceneViewController
implemented via the UIViewControllerRepresentable
protocol?