带有SceneKit的SwiftUI:如何使用视图中的按钮操作来操纵基础场景

时间:2019-10-04 12:37:00

标签: swift scenekit swiftui

我正在尝试使用基础的SceneKit场景/视图实现一个非常基本的SwiftUI应用。 SwiftUI视图中的按钮应操纵场景的内容,反之亦然,场景的内容应确定按钮是处于活动状态还是处于非活动状态。

是的,我已经阅读并观看了226: Data Flow through SwiftUI231: Integrating SwiftUI的Apple开发人员会议。然后我通过苹果tutorials工作。但我在这里一点也不迷路。任何提示和指示,不胜感激。

代码如下:

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

import SwiftUI

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

            HUDView()
        }
    }
}

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())
    }
}

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()
        }
    }
}

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

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)
    }
}

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

basic swift app with SceneKit scene

SceneKit场景已正确渲染,我可以在屏幕上操作相机。

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

执行此任务的最佳方法是什么?我应该在Coordinator中实现SceneView吗?我是否应该使用通过SceneViewController协议实现的单独的UIViewControllerRepresentable

3 个答案:

答案 0 :(得分:1)

我找到了使用@EnvironmentalObject的解决方案,但是我不确定是否是正确的方法。因此,对此表示赞赏。

首先,我将SCNScene移到它自己的类中,并将其设置为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
    }
}

我将此Scene作为.environmentalObject()注入应用程序,因此它可用于所有视图:

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进行了些微改动,以使用单独的SceneView来调用UIViewRepresentable(a Scene):

struct MainView: View {
    @EnvironmentObject var scene: Scene

    var body: some View {
        ZStack {
            SceneView(scene: self.scene.scene)
            HUDView()
        }
    }
}

然后,我将Scene作为HUDView@EnvironmentalObject使用,因此我可以引用场景及其方法,并从Button动作中调用它们。另一个效果是,我可以查询Scene辅助方法来确定Button是否应该处于活动状态:

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()
        }
    }
}

这是ButtonView代码,它使用@Binding设置其活动状态(不确定@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)

    }
}

无论如何,该代码现在可以正常工作。有什么想法吗?

答案 1 :(得分:1)

有很多方法可以做到这些,而您显然已经解决了-恭喜!

您可以查看有关58103566的游戏设计布局的信息。我敢肯定会有专家不同意,这很好,我不想引起争论,只是认为这可能会在将来节省您几步。

从多个VC中,我经常操纵游戏对象和按钮,通常不会使水变得太多。

希望有帮助。

答案 2 :(得分:0)

我回答了一个非常相似的问题here。这个想法是,您创建一个PassthroughSubject,您的父视图(拥有按钮的视图)将向下发送事件,而您的UIViewRespresentable将订阅这些事件。