ARKit 添加 UIView xib 文件 Swift 4
ARKit adding UIView xib file Swift 4
当我检测到我的锚点时,我将一个 xib UIView 文件作为 material 添加到我的 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 我的应用程序不响应任何触摸事件
好的,那么第一件事:
您不能将 UIView
添加为 SCNMaterialProperty
的 contents
。阅读 here.
我在这里猜测,但这可能是你看到的 freeze/crash 的原因,因为当你弹出它所在的控制器并尝试清理 material 时场景被取消实际上是坏的(它会把它当作其他类型)。
一般来说行
myView = (Bundle.main.loadNibNamed("ARViewOne", owner: nil, options: nil)![0] as? UIView)!
在我看来你对此有点陌生(完全没问题!)。你是 force-unwrapping 和 force-casting(奇怪的是,as? UIView!
基本上就是 as! UIView
),这总是很危险的。您确定您的 xib 中的第一个顶级 object 是 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) 您只想要 "view's looks",即节点上的一种 "screenshot"。它将是一个静止图像,显然,您无法将视图的整个行为(附加的手势识别器等)放到这样的节点上。对于这些事情,我建议提出一个新问题(在您满意地完成此操作之后)。
编辑以回应 Fabio 的评论:
我忘记了 renderer(_:nodeFor:)
在另一个 queue/thread 上被调用。正如警告 "UIView.bounds must be used from main thread only" 所说,您确实应该只在主线程上生成视图图像。我更改了上面的代码以反映这一点。请记住 swift 本质上不是线程安全的,上面的工作是因为 viewDidLoad()
将在渲染器开始发送 renderer(_:nodeFor:)
调用之前调用(在主线程上)。因此,您可以确定在检索图像时不会发生访问冲突,但这意味着您不应该在主线程的某处修改所述图像。
如果您需要以某种方式即时更改 image/create 它,还有一个替代方案。我个人会使用委托的其他方法 renderer(_:didAdd:for:)
,而不是自己在 renderer(_:nodeFor:)
中创建节点。此方法不需要 return 值,因此它看起来像这样:
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:)
,它不期望 return 值。所以从那里我 "switch to the main queue" 并添加必要的 child。
我很确定这是可行的,IIRC SceneKit 在其渲染线程中自动批处理添加(即它在节点 addChildNode()
中实际执行的操作)。因此,上面的所有 "prep work" 都发生在主线程上,您可以在其中安全地使用视图的 bounds
来创建图像。节点已设置好,当您添加它时,SceneKit 会负责在其渲染线程上正确地执行此操作。
你应该不做的是简单地在你的renderer(_:nodeFor:)
方法中放置一个DispatchQueue.main.async
并改变material的扩散内容那里。那将意味着您从不同的线程修改添加的节点,但您不能确定它在没有冲突的情况下工作。它可能看起来像工作,我不知道 SceneKit 是否以某种方式防止它,但我不会依赖它。无论如何,这是糟糕的风格。
我希望这一切足以使您的项目顺利进行。 :)
当我检测到我的锚点时,我将一个 xib UIView 文件作为 material 添加到我的 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 我的应用程序不响应任何触摸事件
好的,那么第一件事:
您不能将 UIView
添加为 SCNMaterialProperty
的 contents
。阅读 here.
我在这里猜测,但这可能是你看到的 freeze/crash 的原因,因为当你弹出它所在的控制器并尝试清理 material 时场景被取消实际上是坏的(它会把它当作其他类型)。
一般来说行
myView = (Bundle.main.loadNibNamed("ARViewOne", owner: nil, options: nil)![0] as? UIView)!
在我看来你对此有点陌生(完全没问题!)。你是 force-unwrapping 和 force-casting(奇怪的是,as? UIView!
基本上就是 as! UIView
),这总是很危险的。您确定您的 xib 中的第一个顶级 object 是 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
扩展名取自
这假设 a) 您的 xib 确实正确并且 b) 您只想要 "view's looks",即节点上的一种 "screenshot"。它将是一个静止图像,显然,您无法将视图的整个行为(附加的手势识别器等)放到这样的节点上。对于这些事情,我建议提出一个新问题(在您满意地完成此操作之后)。
编辑以回应 Fabio 的评论:
我忘记了 renderer(_:nodeFor:)
在另一个 queue/thread 上被调用。正如警告 "UIView.bounds must be used from main thread only" 所说,您确实应该只在主线程上生成视图图像。我更改了上面的代码以反映这一点。请记住 swift 本质上不是线程安全的,上面的工作是因为 viewDidLoad()
将在渲染器开始发送 renderer(_:nodeFor:)
调用之前调用(在主线程上)。因此,您可以确定在检索图像时不会发生访问冲突,但这意味着您不应该在主线程的某处修改所述图像。
如果您需要以某种方式即时更改 image/create 它,还有一个替代方案。我个人会使用委托的其他方法 renderer(_:didAdd:for:)
,而不是自己在 renderer(_:nodeFor:)
中创建节点。此方法不需要 return 值,因此它看起来像这样:
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:)
,它不期望 return 值。所以从那里我 "switch to the main queue" 并添加必要的 child。
我很确定这是可行的,IIRC SceneKit 在其渲染线程中自动批处理添加(即它在节点 addChildNode()
中实际执行的操作)。因此,上面的所有 "prep work" 都发生在主线程上,您可以在其中安全地使用视图的 bounds
来创建图像。节点已设置好,当您添加它时,SceneKit 会负责在其渲染线程上正确地执行此操作。
你应该不做的是简单地在你的renderer(_:nodeFor:)
方法中放置一个DispatchQueue.main.async
并改变material的扩散内容那里。那将意味着您从不同的线程修改添加的节点,但您不能确定它在没有冲突的情况下工作。它可能看起来像工作,我不知道 SceneKit 是否以某种方式防止它,但我不会依赖它。无论如何,这是糟糕的风格。
我希望这一切足以使您的项目顺利进行。 :)