我可以让 SCNBox 可点击和交互吗?

Posted

技术标签:

【中文标题】我可以让 SCNBox 可点击和交互吗?【英文标题】:Can I make SCNBox tappable and interactive? 【发布时间】:2021-12-31 18:56:39 【问题描述】:

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

这就是我目前尝试将 Box Scene 插入 SwiftUI 视图的方式,然后该视图将进入现有应用程序的视图层次结构。

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

【问题讨论】:

如何在 SwiftUI 视图中显示 SCNBox?您应该显示该代码。 目前我正试图弄清楚如何做到这一点。我想将 SCNBox 集成到带有标签的现有应用程序中。目标是让此框进入其中一个选项卡(当前使用 TabView),一旦您点击框的一侧,您将获得 (a) 一个弹出窗口,和/或 (b) 重定向到另一个应用程序中的选项卡。问题是将盒子集成到现有应用程序中(目前它们是单独的 Xcode 项目。提前致谢! 你将需要展示比你已经完成的更多的代码。目前你没有提供足够的代码来帮助你。 刚刚编辑了我的原始问题以添加您在上面请求的代码......虽然它没有编译。 您可能需要继承 SKScene 并覆盖 touchesBegan 以捕获触摸事件。你可以将闭包传递给你的子类,这样你就可以对你的触摸事件采取行动。 【参考方案1】:

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

BoxNode 是一个自定义SCNNode,它使用SCNBox 作为其几何形状。每一面都设置为不同的颜色,以便于查看,如果您想要展示更详细的内容,可以使用UIImages。由于我们使用的是 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 设置在SCNView 内部UIViewController 内部。 SCNView 固定在 ViewController 的边缘,我们覆盖 touchesBegan 以便我们可以与 SceneView 交互。我还创建了一个变量sideTapped,因为这将允许您注入一个回调,以便您可以判断哪一侧已被点击了您的立方体。

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 视图中。

struct ContentView: View 

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

最终结果应该是这样的:

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

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

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


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

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

【讨论】:

非常感谢!这正是我所需要的,而且效果很好。一个问题 - 你知道如何将被点击的一侧重定向到应用程序中的现有视图吗?我尝试了以下但没有成功:` struct ContentView: View var body: some View GSViewControllerRepresentable side in if side = 1 FirstView() else AnotherView() .frame(width: 200, height: 200 ) ` 这永远不会起作用,因为您获得侧面的回调不是 ViewBuilder。您需要更新 ContentView 中的一些状态值,然后驱动视图的变化。此链接中的第二个代码示例应该可以帮助您hackingwithswift.com/quick-start/swiftui/…,尽管它使用字符串它应该适用于使用 Ints。 GSViewControllerRepresentable 基本上是一个花哨的按钮,它在其闭包中返回一个值,您可以将该值设置为链接示例中的选择。 现在当我点击一侧时,View 会在底部弹出,而 Cube 仍然在屏幕顶部。您知道如何将其调整为完全将 ** 重定向到另一个视图吗?我当前的代码如下所示:` struct CubeView: View @State private var selection: Int? = nil var body: some View GSViewControllerRepresentable side in selection = side .frame(width: 250, height: 350) if selection == 1 FirstView() else ` 我在上一条评论中列出的教程是你需要做的。 谢谢——我就是这么做的。但是,View 并没有取代 Cube,而是出现在 Cube 下方,并且 Cube 仍然可见。想知道如何从视图中删除 Cube,但由于我不熟悉 ViewControllerRepresentable 行为,我不确定如何调试它。再次感谢!

以上是关于我可以让 SCNBox 可点击和交互吗?的主要内容,如果未能解决你的问题,请参考以下文章

vue和c语言可以交互吗

emacs 如何让buffer变的可交互 让buffer操作shell

iOS:我可以以编程方式决定与哪个组件交互(滚动)吗?

检查 DOM 元素是不是可以交互

推送通知作为与 Facebook Messenger Bot 的首次交互?

检查是不是可以与 DOM 元素交互