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

Posted

技术标签:

【中文标题】带有 SceneKit 的 SwiftUI:如何使用视图中的按钮操作来操作底层场景【英文标题】:SwiftUI with SceneKit: How to use button action from view to manipulate underlying scene 【发布时间】:2020-02-02 19:16:16 【问题描述】:

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

是的,我已经阅读并观看了 Apple 开发者会议 226: Data Flow through SwiftUI 和 231: Integrating SwiftUI。我通过 Apple tutorials 工作。但我在这里有点迷失。任何提示和方向表示赞赏。

代码如下:

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

import SwiftUI

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

            HUDView()
        
    

SceneView 通过UIViewRepresentable 协议集成了SCNView。场景有两个函数可以在场景中添加和移除盒子:

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)
    

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

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

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

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

【问题讨论】:

【参考方案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 被略微更改为调用 SceneViewUIViewRepresentable),并为环境使用单独的 Scene

struct MainView: View 
    @EnvironmentObject var scene: Scene

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

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

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 inHUDView 的正确顺序):

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)

    

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

【讨论】:

【参考方案2】:

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

【讨论】:

感谢您的提示,但我尝试@EnvironmentalObject 建立从场景到 HUD 的双向关系。请参阅我的单独答案。【参考方案3】:

有很多方法可以做到这一点,你显然已经解决了 - 恭喜!

您可以查看这篇关于游戏设计布局的帖子58103566。我敢肯定会有专家不同意这很好,我不想引起争论,只是认为它可能会在未来为您节省一些步骤。

来自多个 VC,我经常操作游戏对象和按钮,通常不会把水弄得太多。

希望对你有帮助。

【讨论】:

谢谢,会考虑的。一开始只是一个单一的场景应用程序。您是否推荐任何专门针对 SceneKit 的书籍/文章/博客?顺便说一句,我添加了指向您的帖子 ID 的直接链接。 谢谢,这个人很好:raywenderlich.com - 其余的我从 StackO 获得。

以上是关于带有 SceneKit 的 SwiftUI:如何使用视图中的按钮操作来操作底层场景的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI – 将数据从 SwiftUIView 传递到 SceneKit

SceneKit - 如何知道带有 MDLObject 的 SCNNode 是不是已加载并可见?

带有 SceneKit SCNProgram 的金属着色器

如何使用 SwiftUI 更新不同的视图?

如何在 SceneKit 中使用 SCNBufferBindingBlock?

有啥方法可以将视图从 swiftUI 更改为 UIKit?