ARKit 添加 UIView xib 文件 Swift 4

4

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

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个回答

4

首先,你不能将一个UIView作为SCNMaterialPropertycontents添加进去。在这里阅读相关文档:https://developer.apple.com/documentation/scenekit/scnmaterialproperty/1395372-contents

我猜测,这很可能是你看到的冻结/崩溃的原因,因为当你弹出控制器并尝试清理那个实际上已经损坏的材质时,场景会被释放(它将把它视为其他类型)。

一般来说,这行代码:

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扩展是从这里获取的,所以感谢Naveed J.)

假设a)您的xib确实正确,b)您只想要“视图外观”,即节点上的一种“截图”。显然,它将是静止的图像,您无法将视图的整个行为(附加的手势识别器等)放入此类节点中。对于这些内容,我建议在您满意本文后提出新问题。

根据Fabio的评论进行编辑:

我忘记了renderer(_:nodeFor :) 在不同的队列/线程上调用。正如警告“UIView.bounds必须仅从主线程使用”所说,您确实应该仅在主线程上生成视图图像。我更改了上面的代码以反映这一点。请记住,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:),它不需要返回值。因此,我"切换到主队列"并添加所需的子节点。
我很确定这个方法是有效的,如果我没记错,SceneKit会自动批量处理添加(即在节点的addChildNode()内部实际进行的操作)。因此,所有的"预处理工作"都在主线程完成,在那里你可以安全地使用视图的bounds来创建图像。当节点设置好时,再将其添加,SceneKit会在其渲染线程上妥善地处理。
你不应该简单地在renderer(_:nodeFor:)方法中放置一个DispatchQueue.main.async,并在其中更改材质的漫射内容。这意味着你从不同的线程修改了添加的节点,但是你无法确保这样做不会发生冲突。它看起来可能有效,我不知道SceneKit是否以某种方式对此进行了保护,但我不会依赖于它。在我看来,这是不好的习惯。
希望这一切足以使你的项目工作。 :)

这是一个很好的答案,但我遇到了这个线程警告,当我使用扩展将UIView渲染为图像时...UIView.bounds只能从主线程中使用,你有什么解决办法吗? - Fabio

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接