Overlaying different swiftUI views depending on the image detected with ARkit app.

I have code that detects images via ARkit. When an image is detected, a swiftUI view is placed on top of the image. My code can detect the images, however I am only able to overlay one swiftUI view. I'd like the code to be responsive so that it can identify the image and place the appropriate view on top of it. Code below:


Code for SwiftUI imageOne.swift file
Code Block
import SwiftUI
struct imageOne: View {
var body: some View {
Text("hello imageOne")
}
}

Code for SwiftUI imageTwo.swift file
Code Block
import SwiftUI
struct imageTwo: View {
var body: some View {
Text("hello imageTwo")
}
}


Code for ViewController.swift file

Code Block swift
import UIKit
import SceneKit
import ARKit
import SwiftUI
class ViewController: UIViewController, ARSCNViewDelegate {
@IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARImageTrackingConfiguration()
guard let trackingImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else {
fatalError("Couldn't load tracking images")
}
configuration.trackingImages = trackingImages
configuration.maximumNumberOfTrackedImages = 2
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
guard let imageAnchor = anchor as? ARImageAnchor else {return nil}
let plane = SCNPlane(width: imageAnchor.referenceImage.physicalSize.width,
height: imageAnchor.referenceImage.physicalSize.height)
let planeNode = SCNNode(geometry: plane)
planeNode.eulerAngles.x = -.pi / 2
imageOneController(for: planeNode)
let node = SCNNode()
node.addChildNode(planeNode)
return node
}
func imageOneController(for node: SCNNode) {
let imageOneView = UIHostingController(rootView: imageOne())
DispatchQueue.main.async {
imageOneView.willMove(toParent: self)
self.addChild(imageOneView)
imageOneView.view.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
self.view.addSubview(imageOneView.view)
self.showImageOne(hostingVC: imageOneView, on: node)
}
}
func imageTwoController(for node: SCNNode) {
let imageTwoView = UIHostingController(rootView: imageTwo())
DispatchQueue.main.async {
imageTwoView.willMove(toParent: self)
self.addChild(imageTwoView)
imageTwoView.view.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
self.view.addSubview(imageTwoView.view)
self.showImageTwo(hostingVC: imageTwoView, on: node)
}
}
func showImageOne(hostingVC: UIHostingController<imageOne>, on node: SCNNode) {
let material = SCNMaterial()
hostingVC.view.isOpaque = false
material.diffuse.contents = hostingVC.view
node.geometry?.materials = [material]
hostingVC.view.backgroundColor = UIColor.clear
}
func showImageTwo(hostingVC: UIHostingController<imageTwo>, on node: SCNNode) {
let material = SCNMaterial()
hostingVC.view.isOpaque = false
material.diffuse.contents = hostingVC.view
node.geometry?.materials = [material]
hostingVC.view.backgroundColor = UIColor.clear
}
}


The images on my AR Resource are also called imageOne and imageTwo, same each of the swiftUI views


Answered by brandonK212 in 630340022
Hi @Josedv,

Thanks for your reply. For some reason, I seem to be unable to edit my original post, but I caught a few typos that are likely what is preventing you from running the code (including what is causing the error message you indicated). For brevity, make a few changes;

Whereas I had noted your function should be;

Code Block
func imageController(for node: SCNNode, imageName: imageName)

You will want to change that to;
Code Block
func imageController(for node: SCNNode, imageName: String)

That was a typo on my part, as imageName is not a type; the imageName you are providing from your ARReferenceImage is a String.

Subsequently, there are two other things you should change (one for clarity and one that actually is going to lead to an error). In the same function, which is now func imageController(for node: SCNNode, imageName: String), you will want to change the name of the first variable, defining the UIHostingController, as the name imageView is being used for both defining the UIHostingController and your SwiftUI view, which is confusing. Subsequently, when calling the SwiftUI view, the name of the variable you are passing should be imageName and not name. As such, that whole function should look like this;

Code Block
func imageController(for node: SCNNode, imageName: String) {
let imgView = UIHostingController(rootView: imageView(imageName: imageName))
DispatchQueue.main.async {
imgView.willMove(toParent: self)
self.addChild(imgView)
imgView.view.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
self.view.addSubview(imgView.view)
self.showImageView(hostingVC: imgView, on: node)
}
}

This, alongside what was mentioned yesterday, should get you up and running. I wish I could edit my previous post (maybe I'm just overlooking how to do that), but I gave all of this code a try and was able to compile without any issues.

Based on your desired goal, there are probably two things I'd change in this implementation to make it easier and more extensible to work with multiple ARReferenceImages. In func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor), I would likely retrieve the name of the image, and set up a statement that would allow me to call a SwiftUI view while passing the name of the image. Subsequently, based on your sample code, there really isn't a need to have two SwiftUI views; you could have one SwiftUI that takes a property (such as a name, an index, etc.) and shows the relevant text on the screen.

For example, I might modify your func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) function to appear like so;
Code Block
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
guard let imageAnchor = anchor as? ARImageAnchor else {return nil}
let plane = SCNPlane(width: imageAnchor.referenceImage.physicalSize.width,
height: imageAnchor.referenceImage.physicalSize.height)
let planeNode = SCNNode(geometry: plane)
planeNode.eulerAngles.x = -.pi / 2
if let imageName = imageAnchor.referenceImage.name {
imageController(for: planeNode, imageName: imageName)
}
let node = SCNNode()
node.addChildNode(planeNode)
return node
}


Then, you could modify your func imageOneController(for node: SCNNode) function, like so;

Code Block
func imageController(for node: SCNNode, imageName: imageName) {
let imageView = UIHostingController(rootView: imageView(name: imageName))
DispatchQueue.main.async {
imageView.willMove(toParent: self)
self.addChild(imageView)
imageView.view.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
self.view.addSubview(imageView.view)
self.showImageView(hostingVC: imageView, on: node)
}
}


At that point, you could then show your image view in func showImageOne(hostingVC: UIHostingController<imageOne>, on node: SCNNode), as you are doing now, but doing it in a general way so that you do not need to create new views for each image. As such, I would modify func showImageOne(hostingVC: UIHostingController<imageOne>, on node: SCNNode) to be something like;

Code Block
func showImageView(hostingVC: UIHostingController<imageView>, on node: SCNNode) {
let material = SCNMaterial()
hostingVC.view.isOpaque = false
material.diffuse.contents = hostingVC.view
node.geometry?.materials = [material]
hostingVC.view.backgroundColor = UIColor.clear
}


Lastly, you would want to modify your imageOne SwiftUI struct to be something more generic, like;

Code Block
import SwiftUI
struct imageView: View {
var imageName: String
var body: some View {
Text("hello \(imageName)")
}
}


In general, I am making some assumptions about what you are trying to do, but if you were to ever add more ARReferenceImages to your app, based on the methodology you have in place here, you'd have to keep creating new SwiftUI views and new functions for each one. Using some sort of an identifier (like the image's name) will allow you to more generally create your views and display them accordingly.

Thanks for the reply. This seems exactly like what I am trying to achieve. I made the changes to the code as you suggested, however it seems that I am having issues with imageName. I am getting "Use if undeclared type 'imageName'". Is there anything I am missing. Below is my code as it looks now:


Code Block
 func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        guard let imageAnchor = anchor as? ARImageAnchor else {return nil}
        let plane = SCNPlane(width: imageAnchor.referenceImage.physicalSize.width,
                         height: imageAnchor.referenceImage.physicalSize.height)
        let planeNode = SCNNode(geometry: plane)
        planeNode.eulerAngles.x = -.pi / 2
        if let imageName = imageAnchor.referenceImage.name {
            imageController(for: planeNode, imageName: imageName)
        }
        let node = SCNNode()
        node.addChildNode(planeNode)
        return node
        }
   
    func imageController(for node: SCNNode, imageName: imageName) {
        let imageView = UIHostingController(rootView: imageView(name: imageName))
    DispatchQueue.main.async {
        imageView.willMove(toParent: self)
        self.addChild(imageView)
        imageView.view.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
        self.view.addSubview(imageView.view)
        self.showImageView(hostingVC: imageView, on: node)
    }
    
    }
    
func showImageView(hostingVC: UIHostingController<imageView>, on node: SCNNode) {
        let material = SCNMaterial()
        hostingVC.view.isOpaque = false
        material.diffuse.contents = hostingVC.view
        node.geometry?.materials = [material]
        hostingVC.view.backgroundColor = UIColor.clear
    }
}

Accepted Answer
Hi @Josedv,

Thanks for your reply. For some reason, I seem to be unable to edit my original post, but I caught a few typos that are likely what is preventing you from running the code (including what is causing the error message you indicated). For brevity, make a few changes;

Whereas I had noted your function should be;

Code Block
func imageController(for node: SCNNode, imageName: imageName)

You will want to change that to;
Code Block
func imageController(for node: SCNNode, imageName: String)

That was a typo on my part, as imageName is not a type; the imageName you are providing from your ARReferenceImage is a String.

Subsequently, there are two other things you should change (one for clarity and one that actually is going to lead to an error). In the same function, which is now func imageController(for node: SCNNode, imageName: String), you will want to change the name of the first variable, defining the UIHostingController, as the name imageView is being used for both defining the UIHostingController and your SwiftUI view, which is confusing. Subsequently, when calling the SwiftUI view, the name of the variable you are passing should be imageName and not name. As such, that whole function should look like this;

Code Block
func imageController(for node: SCNNode, imageName: String) {
let imgView = UIHostingController(rootView: imageView(imageName: imageName))
DispatchQueue.main.async {
imgView.willMove(toParent: self)
self.addChild(imgView)
imgView.view.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
self.view.addSubview(imgView.view)
self.showImageView(hostingVC: imgView, on: node)
}
}

This, alongside what was mentioned yesterday, should get you up and running. I wish I could edit my previous post (maybe I'm just overlooking how to do that), but I gave all of this code a try and was able to compile without any issues.

@brandonk212

Thanks for the quick reply. I tried the code with the new fixes and it works perfectly. Really appreciate your help, being able to work with just one swiftUI file is going to save me a lot of headaches down the road. Very helpful.
Overlaying different swiftUI views depending on the image detected with ARkit app.
 
 
Q