ARKit 添加 UIView xib 文件 Swift 4

Posted

技术标签:

【中文标题】ARKit 添加 UIView xib 文件 Swift 4【英文标题】:ARKit adding UIView xib file Swift 4 【发布时间】:2018-10-03 22:08:21 【问题描述】:

当我检测到我的锚点时,我向我的 SCNBox 添加了一个 xib UIView 文件作为材料,但是当我关闭这个视图控制器时,应用程序冻结了,这是我的代码:

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】:

好的,那么首先:

您不能将UIView 添加为contentsSCNMaterialProperty。阅读here。

我在这里猜测,但这可能是您看到的冻结/崩溃的原因,因为当您弹出它所在的控制器并尝试清理实际损坏的材料时,场景会被破坏(它会处理它作为其他类型)。

一般来说行

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

在我看来,您对此有点陌生(这完全没问题!)。你在强制展开和强制转换(以一种奇怪的方式,as? UIView! 基本上只是as! UIView),这总是很危险的。你确定你的xib中的第一个***对象是UIView吗?此外,最初实例化的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)
        
    

UIView 扩展名取自 here,感谢 Naveed J.)

这假设a)您的xib确实是正确的并且b)您只想要“视图的外观”,即节点上的一种“屏幕截图”。这将是一个静止图像,显然,您无法将视图的整个行为(附加的手势识别器等)放到这样的节点上。对于这些事情,我建议您提出一个新问题(在您满意地完成此操作之后)。

根据 Fabio 的评论进行编辑:

我忘记了 renderer(_:nodeFor:) 在不同的队列/线程上被调用。您确实应该只在主线程上生成视图图像,因为警告“UIView.bounds must be used only from main thread”说。我更改了上面的代码以反映这一点。请记住,swift 本质上不是线程安全的,因为viewDidLoad() 将在渲染器开始发送renderer(_:nodeFor:) 调用之前(在主线程上)被调用。因此,您可以确保在检索图像时不会遇到访问冲突,但这意味着您不应该在主线程的某处修改所述图像。

如果您需要以某种方式更改图像/动态创建图像,则可以使用另一种方法。我个人会使用代理的另一种方法renderer(_:didAdd:for:),而不是自己在renderer(_: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

通过从您的委托中省略renderer(_:nodeFor:),ARKit 会为您创建一个空节点。基本上,这与您在renderer(_:nodeFor:) 中所做的事情相同。然后它调用renderer(_:didAdd:for:),它不期望返回值。所以从那里我“切换到主队列”并添加必要的孩子。

我很确定这可行,IIRC SceneKit 会在其渲染线程中自动批量添加(即它在节点的addChildNode() 中实际执行的操作)。因此,上述所有“准备工作”都发生在主线程上,您可以安全地使用视图的bounds 创建图像。节点已设置好,当您添加它时,SceneKit 会在其渲染线程上正确执行此操作。

您应该做的只是在您的renderer(_:nodeFor:) 方法中添加一个DispatchQueue.main.async 并在其中更改材质的漫反射内容。这意味着您从不同的线程修改添加的节点,但您不能确定它可以在没有冲突的情况下工作。它可能看起来很有效,我不知道 SceneKit 是否会以某种方式防范它,但我不会依赖它。无论如何,这是一种糟糕的风格。

我希望这一切足以让您的项目顺利进行。 :)

【讨论】:

这是一个很好的答案,但我有这个胎面警告,当我使用扩展将 UIView 呈现为图像时...... UIView.bounds 必须从主线程使用,你知道如何解决这个问题?

以上是关于ARKit 添加 UIView xib 文件 Swift 4的主要内容,如果未能解决你的问题,请参考以下文章

将 xib 中的子视图添加到自定义 UIView

带有 Xib 文件的 UIView 的子类

如何将 UITableView 作为子视图添加到 xib 中的 UIView?

UIView 旋转没有 XIB

在 xib 中以编程方式自定义 UIView

向一个 UIView 多次添加 xib