带有 SceneKit 的 SwiftUI:如何使用视图中的按钮动作来操作底层场景

dir*_*irk 6 scenekit swift swiftui

我正在尝试使用底层 SceneKit 场景/视图来实现一个非常基本的 SwiftUI 应用程序。SwiftUI 视图中的按钮应该操纵场景的内容,反之亦然,场景的内容应该确定按钮是活动的还是非活动的。

是的,我已经阅读并观看了 Apple 开发人员会议226:通过 SwiftUI 的数据流231:集成 SwiftUI。我按照自己的方式学习了 Apple教程。但我在这里有点迷失。任何提示和方向表示赞赏。

这是代码:

我有一个 MainView,它使用一个SCNView带有HUDView顶部的 SceneKit :

import SwiftUI

struct MainView: View {
    var body: some View {
        ZStack {
            SceneView()

            HUDView()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

SceneView集成了SCNView经由UIViewRepresentable协议。场景有两个函数来添加和移除场景中的框:

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())
    }
}
Run Code Online (Sandbox Code Playgroud)

HUDView按钮与用于在底层场景中添加和删除框的操作捆绑在一起。如果框对象在场景中,则添加按钮应处于非活动状态,只有移除按钮应处于活动状态:

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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这些按钮也相当简单,它们执行一个操作,可选的一个图标以及它们的初始活动/非活动状态:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

生成的应用程序非常简单:

带有 SceneKit 场景的基本 Swift 应用程序

SceneKit 场景渲染正确,我能够在屏幕上操纵相机。

但是如何将按钮动作连接到相应的场景功能呢?我如何让场景通知 HUDView 它的内容(框是否存在),以便它可以设置按钮的活动/非活动状态?

完成这项任务的最佳方法是什么?我应该实现一个CoordinatorinSceneView吗?我应该使用SceneViewController通过UIViewControllerRepresentable协议单独实现吗?

dir*_*irk 2

我找到了一个解决方案,使用@EnvironmentalObject,但我不完全确定这是否是正确的方法。因此,对此发表评论表示赞赏。

\n\n

首先,我将 SCNScene 移入 \xe2\x80\x99s 自己的类中,并将其设为OberservableObject

\n\n
class Scene: ObservableObject {\n    @Published var scene: SCNScene\n\n    init(_ scene: SCNScene = SCNScene()) {\n        self.scene = scene\n        self.scene = setup(scene: self.scene)\n    }\n\n    // code omitted which deals with setting the scene up and adding/removing the box\n\n    // method used to determine if the box node is present in the scene -> used later on \n    func boxIsPresent() -> Bool {\n        return scene.rootNode.childNode(withName: "box", recursively: true) != nil\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我将其Scene作为.environmentalObject(),因此它可用于所有视图:

\n\n
class SceneDelegate: UIResponder, UIWindowSceneDelegate {\n\n    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {\n\n        // Create the SwiftUI view that provides the window contents.\n        let sceneKit = Scene()\n        let mainView = MainView().environmentObject(sceneKit)\n\n        // Use a UIHostingController as window root view controller.\n        if let windowScene = scene as? UIWindowScene {\n            let window = UIWindow(windowScene: windowScene)\n            window.rootViewController = UIHostingController(rootView: mainView)\n            self.window = window\n            window.makeKeyAndVisible()\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

MainView稍微改变一下,调用SceneView(a UIViewRepresentable) 并单独Scene设置环境:

\n\n
struct MainView: View {\n    @EnvironmentObject var scene: Scene\n\n    var body: some View {\n        ZStack {\n            SceneView(scene: self.scene.scene)\n            HUDView()\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

然后我将其Scene作为HUDViewan提供@EnvironmentalObject,这样我就可以引用场景及其方法并从 Button 操作中调用它们。另一个效果是,我可以查询Scene辅助方法来确定按钮是否应该处于活动状态:

\n\n
struct HUDView: View {\n    @EnvironmentObject var scene: Scene\n    @State private var canAddBox: Bool = false\n    @State private var canRemoveBox: Bool = true\n\n    var body: some View {\n        VStack {\n            HStack(alignment: .center, spacing: 0) {\n                Spacer ()\n\n                ButtonView(\n                    action: {\n                        self.scene.addBox()\n                        if self.scene.boxIsPresent() {\n                            self.canAddBox = false\n                            self.canRemoveBox = true\n                        }\n                    },\n                    icon: "plus.square.fill",\n                    isActive: $canAddBox\n                )\n\n                ButtonView(\n                    action: {\n                        self.scene.removeBox()\n                        if !self.scene.boxIsPresent() {\n                            self.canRemoveBox = false\n                            self.canAddBox = true\n                        }\n                    },\n                    icon: "minus.square.fill",\n                    isActive: $canRemoveBox\n                )\n\n            }\n            .background(Color.white.opacity(0.2))\n\n            Spacer()\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

这是 ButtonView 代码,它使用 a@Binding来设置其活动状态(不确定使用@State property in):

\n\n
struct ButtonView: View {\n    let action: () -> Void\n    var icon: String = "square"\n    @Binding var isActive: Bool\n\n    var body: some View {\n        Button(action: action) {\n            Image(systemName: icon)\n                .font(.title)\n                .accentColor(self.isActive ? Color.white : Color.white.opacity(0.5))\n        }\n        .frame(width: 44, height: 44)\n        .disabled(self.isActive ? false: true)\n\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

无论如何,代码现在可以运行了。对此有什么想法吗?

\n