SwiftUI with SceneKit: How to use button action from view to manipulate underlying scene
I found a solution using @EnvironmentalObject
but I am not completely sure, if this is the right approach. So comments on this are appreciated.
First, I moved the SCNScene into it’s own class and made it an OberservableObject
:
class Scene: ObservableObject {
@Published var scene: SCNScene
init(_ scene: SCNScene = SCNScene()) {
self.scene = scene
self.scene = setup(scene: self.scene)
}
// code omitted which deals with setting the scene up and adding/removing the box
// method used to determine if the box node is present in the scene -> used later on
func boxIsPresent() -> Bool {
return scene.rootNode.childNode(withName: "box", recursively: true) != nil
}
}
I inject this Scene
into the app as an .environmentalObject()
, so it is available to all views:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let sceneKit = Scene()
let mainView = MainView().environmentObject(sceneKit)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: mainView)
self.window = window
window.makeKeyAndVisible()
}
}
}
MainView
is slightly altered to call SceneView
(a UIViewRepresentable
) with the separate Scene
for the environment:
struct MainView: View {
@EnvironmentObject var scene: Scene
var body: some View {
ZStack {
SceneView(scene: self.scene.scene)
HUDView()
}
}
}
Then I make the Scene
available to the HUDView
as an @EnvironmentalObject
, so I can reference the scene and its methods and call them from the Button action. Another effect is, I can query the Scene
helper method to determine, if a Button should be active or not:
struct HUDView: View {
@EnvironmentObject var scene: Scene
@State private var canAddBox: Bool = false
@State private var canRemoveBox: Bool = true
var body: some View {
VStack {
HStack(alignment: .center, spacing: 0) {
Spacer ()
ButtonView(
action: {
self.scene.addBox()
if self.scene.boxIsPresent() {
self.canAddBox = false
self.canRemoveBox = true
}
},
icon: "plus.square.fill",
isActive: $canAddBox
)
ButtonView(
action: {
self.scene.removeBox()
if !self.scene.boxIsPresent() {
self.canRemoveBox = false
self.canAddBox = true
}
},
icon: "minus.square.fill",
isActive: $canRemoveBox
)
}
.background(Color.white.opacity(0.2))
Spacer()
}
}
}
Here is the ButtonView code, which used a @Binding
to set its active state (not sure about the correct order for this with the@State property in
HUDView`):
struct ButtonView: View {
let action: () -> Void
var icon: String = "square"
@Binding var isActive: Bool
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.title)
.accentColor(self.isActive ? Color.white : Color.white.opacity(0.5))
}
.frame(width: 44, height: 44)
.disabled(self.isActive ? false: true)
}
}
Anyway, the code works now. Any thoughts on this?