代码之家  ›  专栏  ›  技术社区  ›  Fabio

ARKit添加UIView xib文件Swift 4

  •  5
  • Fabio  · 技术社区  · 6 年前

    当我检测到锚点时,我将一个xib UIView文件作为材料添加到我的SCNBox中,但当我关闭此viewcontroller时,应用程序冻结,这是我的代码:

    var detectedDataAnchor: ARAnchor?
    var myView = UIView()
    
    override func viewDidLoad() {
      super.viewDidLoad()
      myView = (Bundle.main.loadNibNamed("ARViewOne", owner: nil, options: nil)![0] as? UIView)!
    }
    
    
    
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
    
          if self.detectedDataAnchor?.identifier == anchor.identifier {
            let node = SCNNode()
            let box = SCNBox(width: 0.1, height: 0.1, length: 0.1,
                                     chamferRadius: 0.0)
            let imageMaterial = SCNMaterial()
    
            imageMaterial.diffuse.contents = myView
            box.materials = [imageMaterial]
            let cube = SCNNode(geometry: box)
            node.addChildNode(cube)
            return node
    
        }
       return nil
    }
    
     @IBAction func back(_ sender: UIButton) {
         // here the app freezes
         navigationController?.popViewController(animated: true)
      }
    

    当回到我的VC时,我的应用程序不会响应任何触摸事件

    1 回复  |  直到 6 年前
        1
  •  4
  •   Gero    6 年前

    好吧,那么首先:

    UIView 作为 contents SCNMaterialProperty . 读出 here .

    一般来说

    myView = (Bundle.main.loadNibNamed("ARViewOne", owner: nil, options: nil)![0] as? UIView)!
    

    as? UIView! as! UIView ),这总是很危险的。确定xib中的第一个顶级对象是 UIView视图 (英寸 var myView = UIView() 从未使用过,为什么不在这里使用一个可选的(你可以使用一个隐式展开的,尽管我自己不是这个的粉丝)。这样做怎么样:

    var myViewImage: UIImage?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let myView = Bundle.main.loadNibNamed("ARViewOne", owner: nil, options: nil).first as? UIView
        myViewImage = myView?.asImage()
    }
    
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
    
        guard let image = myViewImage, self.detectedDataAnchor?.identifier == anchor.identifier else { return nil }
    
        let node = SCNNode()
        let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.0)
        let imageMaterial = SCNMaterial()
        imageMaterial.diffuse.contents = image
        box.materials = [imageMaterial]
        let cube = SCNNode(geometry: box)
        node.addChildNode(cube)
        return node
    }
    
    // this would be outside your controller class, i.e. the top-level of a swift file
    extension UIView {
        func asImage() -> UIImage {
            let renderer = UIGraphicsImageRenderer(bounds: bounds)
            return renderer.image { rendererContext in
                layer.render(in: rendererContext.cgContext)
            }
        }
    }
    

    (The) UIView视图 here ,所以谢谢纳维德J.)

    这假设a)您的xib确实正确,b)您只需要“视图的外观”,即节点上的一种“屏幕截图”。这将是一个静态图像,很明显,你不能把视图的整个行为(附加的手势识别器等)放到这样的节点上。对于这些事情,我建议你打开一个新的问题(在你满意的完成这个问题之后)。

    针对法比奥的评论编辑:

    我忘了 renderer(_:nodeFor:) 在不同的队列/线程上被调用。您确实应该只在主线程上生成视图图像,正如警告“UIView.bounds must be used from main thread only”所说的那样。我修改了上面的代码来反映这一点。请记住,swift本身并不是线程安全的,上面的工作原理是 viewDidLoad() 将在渲染器开始发送之前调用(在主线程上)

    如果您需要以某种方式更改图像/动态创建它,有一种替代方法。就我个人而言,我会使用代理的其他方法, renderer(_:didAdd:for:) ,而不是自己在中创建节点 渲染器(\:nodeFor:) . 此方法不需要返回值,因此如下所示:

    var myView: UIView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        myView = Bundle.main.loadNibNamed("ARViewOne", owner: nil, options: nil).first as? UIView
    }
    
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        DispatchQueue.main.async {
            self.attachCustomNode(to: node, for: anchor)
        }
    }
    
    func attachCustomNode(to node: SCNNode, for anchor: ARAnchor) {
        guard let theView = myView, self.detectedDataAnchor?.identifier == anchor.identifier else { return }
    
        let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.0)
        let imageMaterial = SCNMaterial()
        imageMaterial.diffuse.contents = theView.asImage()
        box.materials = [imageMaterial]
        let cube = SCNNode(geometry: box)
        node.addChildNode(cube)
        return node
    }
    

    通过省略 渲染器(\:nodeFor:) ARKit从您的委托中为您创建一个空节点。基本上这和你在工作中做的事情是一样的 渲染器(\:nodeFor:) . 然后它会呼叫 ,它不期望返回值。因此,我从那里“切换到主队列”并添加必要的子队列。

    我很确定这是可行的,IIRC SceneKit会自动批处理添加(即它在节点的 addChildNode() bounds 创建图像。节点得到设置,当您添加它时,SceneKit会在其渲染线程上正确地进行设置。

    你应该做什么 DispatchQueue.main.async 渲染器(\:nodeFor:) 方法并更改其中材质的漫反射内容。这意味着您可以从另一个线程修改添加的节点,但是您不能确保这样做没有冲突。它看起来可能有用,我不知道SceneKit是否能防范这种情况,但我不相信这一点。不管怎么说,这是一种糟糕的风格。