swift - 我可以使 SCNBox 可点击并进行交互吗?

标签 swift swiftui scenekit

我正在尝试在 SwiftUI 中使用 SCNBox 创建一个旋转立方体,您可以点击框的每一侧,然后会出现一个不同的弹出屏幕/ View 并显示文本等。我​​有旋转 SCNBox 立方体,但如何制作它可点击和交互式,如何使其重定向到另一个 View ?

这就是我目前尝试将 Box 场景插入到 SwiftUI View 中的方法,然后该 View 将进入现有应用程序的 View 层次结构。

import SwiftUI
import SceneKit

class BoxNode: SCNNode {

    override init() {
        super.init()
        self.geometry = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0.0)
        
        self.geometry?.firstMaterial?.shininess = 50
        
        let action = SCNAction.rotate(by: 360 * CGFloat(Double.pi / 180), around: SCNVector3(x:0, y:1, z:0), duration: 8)
        
        let repeatAction = SCNAction.repeatForever(action)
        
        self.runAction(repeatAction)
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

struct BoxScene: View {
        var scene = SCNScene(named: "MyScene")
        
        var cameraNode: SCNNode? {
                scene?.rootNode.childNode(withName: "camera", recursively: false)
        }
    
        var boxNode: SCNNode? {
                scene?.rootNode.addChildNode(BoxNode())
        }
        
        var body: some View {
                SceneView(
                        scene: scene,
                        pointOfView: cameraNode,
                        options: []
                )
        }
}

但是,此代码无法编译 - 它表示“表达式类型不明确,以下行中没有更多上下文:

var boxNode: SCNNode? {
                scene?.rootNode.addChildNode(BoxNode())
        }

最佳答案

这是一个使用 SCNBox 可能会有所帮助的示例。

BoxNode 是一个自定义 SCNNode,它使用 SCNBox 作为其几何图形。每面都设置为不同的颜色,以便于查看,如果您想显示更复杂的内容,可以使用 UIImage。由于我们使用的是 SceneKitUIKitSwiftUI,请确保在需要的地方导入它们。

class BoxNode: SCNNode {
    
    override init() {
        let length: CGFloat = 5
        let box = SCNBox(width: length, height: length, length: length, chamferRadius: 0)
        
        box.materials = [UIColor.red, .yellow, .green, .blue, .purple, .brown].map {
            let material = SCNMaterial()
            material.diffuse.contents = $0
            material.isDoubleSided = true
            material.transparencyMode = .aOne
            return material
        }
        
        super.init()
        
        self.geometry = box
        self.name = "boxNode"
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

然后在自定义 SCNScene 中使用它,它将 BoxNode 添加为根节点并设置旋转动画并旋转立方体。

class GameScene: SCNScene {
    
    override init() {
        super.init()
        
        let cubeNode = BoxNode()
        self.rootNode.addChildNode(cubeNode)
        
        let xAngle = SCNMatrix4MakeRotation(.pi / 3.8, 1, 0, 0)
        let zAngle = SCNMatrix4MakeRotation(-.pi / 4, 0, 0, 1)
        cubeNode.pivot = SCNMatrix4Mult(SCNMatrix4Mult(xAngle, zAngle), cubeNode.transform)
        
        // Rotate the cube
        let animation = CAKeyframeAnimation(keyPath: "rotation")
        animation.values = [SCNVector4(1, 1, 0.3, 0 * .pi),
                            SCNVector4(1, 1, 0.3, 1 * .pi),
                            SCNVector4(1, 1, 0.3, 2 * .pi)]
        animation.duration = 5
        animation.repeatCount = HUGE
        cubeNode.addAnimation(animation, forKey: "rotation")
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

然后将 GameScene 设置在 UIViewController 内的 SCNView 内。 SCNView 固定在 ViewController 的边缘,我们重写 touchesBegan 以便我们可以与 SceneView 交互。我还创建了一个变量 sideTapped,因为这将允许您注入(inject)回调,以便您可以知道立方体上的哪一侧被点击。

class GameSceneViewController: UIViewController {
    
    private let sceneView: SCNView = .init(frame: .zero)
    private let scene = GameScene()
    
    var sideTapped: ((Int) -> Void)?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        sceneView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(sceneView)
        
        NSLayoutConstraint.activate([
            sceneView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            sceneView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            sceneView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            sceneView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        ])
        sceneView.scene = scene
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touchPoint = touches.first?.location(in: sceneView) else { return }
        guard let hitTestResult = sceneView.hitTest(touchPoint, options: nil).first else { return }
        guard hitTestResult.node is BoxNode else { return }
        
        sideTapped?(hitTestResult.geometryIndex)
    }
}

当我们在 SwiftUI 中使用它时,我们需要一个 UIViewControllerRepresentable。这里我们将 sideTapped 函数传递给 GameSceneViewController

struct GSViewControllerRepresentable: UIViewControllerRepresentable {
    
    let sideTapped: ((Int) -> Void)?
    
    func makeUIViewController(context: Context) -> GameSceneViewController {
        let vc = GameSceneViewController()
        vc.sideTapped = sideTapped
        return vc
    }
    
    func updateUIViewController(_ uiViewController: GameSceneViewController, context: Context) {
    
    }
}

现在我们可以将其添加到 SwiftUI View 中。

struct ContentView: View {

    var body: some View {
        GSViewControllerRepresentable { side in
            print(side)
            // Perform additional logic here
        }
        .frame(width: 200, height: 200)
    }
}

最终结果应该如下所示:

enter image description here

点击立方体的一侧将打印一个从 0 到 5 的数字,代表您点击的一侧。根据所使用的 Material ,数字应与 Material 的索引相对应,因此在这种情况下:

  • 0 为红色
  • 1 是黄色
  • 2 是绿色
  • 3 是蓝色
  • 4 是紫色
  • 5 是棕色

这应该会给你一些你应该能够关闭导航的东西。


我不是 SceneKit 方面的专家,但我周末正在玩它,并且几乎完成了您所询问的事情。希望这能给您一个想法,有很多方法可以实现您想做的事情,这是一种可能性。

如果您想要滑动立方体来改变面,而不仅仅是让它自行旋转,您可能需要看看这个 answer .

关于swift - 我可以使 SCNBox 可点击并进行交互吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70545002/

相关文章:

ios - 如果节点具有 currentuser.uid 对象并显示在我的 UITableViewCell 中,则从 Firebase 中的节点检索对象

ios - 无法从 HealthKit 获取合理格式的 HRV 读数

macos - 如何在 SwiftUI 中获取窗口坐标?

ios - SceneKit 纹理

swift - UITextField 在输入之前显示 "Optional("")"

ios - 新模型的架构版本更新

swift - LSP 和 SwiftUI

swift - 确保核心数据 @FetchRequest 变量动态运行需要哪些代码更改(附加代码)?

ios - Swift iOS SceneKit 如何使用 NSSecureCoding 对 SCNMatrix4 进行安全编码?

objective-c - SceneKit跟随相机?