12

I am working on an AR project using ARKit.

I want to add a UIView to ARKit Scene. When I tap on an object, I want to get information as a "pop-up" next to the object. This information is in a UIView.

Is it possible to add this UIView to ARKit Scene? I set up this UIView as a scene and what can I do then? Can I give it a node and then add it to the ARKit Scene? If so, how it works?

Or is there another way?

Thank you!

EDIT: Code of my SecondViewController

class InformationViewController: UIViewController {

    @IBOutlet weak var secondView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view = secondView
    }
}

EDIT 2: Code in firstViewController

guard let secondViewController = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController else {
    print ("No secondController")
    return
}

let plane = SCNPlane(width: CGFloat(0.1), height: CGFloat(0.1))       
plane.firstMaterial?.diffuse.contents = secondViewController.view

let node = SCNNode(geometry: plane)

I only get a white screen of a plane, not the view.

Yash Rathod
  • 141
  • 1
  • 7
Informatics
  • 155
  • 1
  • 10
  • Do you mean that you want the information to be displayed in 3D, in the real world? Or that you want the information to be displayed with a segue to another view controller? – Jordan Feb 09 '18 at 08:24
  • I want to place it in 3D in the real world. – Informatics Feb 09 '18 at 08:26
  • As Lësha mentioned above that works very nicely up until iOS 12.1.x Afterwards, what ends up happening, if you need to ever dismiss the UIViewController that presents the UIViews inside of a SCNPlane, the top two thirds of your screen will become unresponsive to touch down events. Submitted this bug earlier today. – Teodor Iuliu Radu Dec 09 '18 at 20:48
  • @TeodorIuliuRadu did you find out more meanwhile? I'm on 12.2 beta and recogniser don't work at all for me ._. – thisIsTheFoxe Mar 20 '19 at 16:39
  • it's now this easy https://stackoverflow.com/a/74380559/294884 – Fattie Nov 09 '22 at 19:55

3 Answers3

19

The simplest (although undocumented) way to achieve that is to set a UIView backed by a view controller as diffuse contents of a material on a SCNPlane (or any other geometry really, but it works best with planes for obvious reasons).

let plane = SCNPlane()
plane.firstMaterial?.diffuse.contents = someViewController.view
let planeNode = SCNNode(geometry: plane)

You will have to persist the view controller somewhere otherwise it's going to be released and plane will not be visible. Using just a UIView without any UIViewController will throw an error.

The best thing about it is that it keeps all of the gestures and practically works just as a simple view. For example, if you use UITableViewController's view you will be able to scroll it right inside a scene.

I haven't tested it on iOS 10 and lower, but it's been working on iOS 11 so far. Works both in plain SceneKit scenes and with ARKit.

Lësha Turkowski
  • 1,361
  • 1
  • 10
  • 21
  • didnt knew about it. I have to try this – Alok Subedi Feb 10 '18 at 04:21
  • If I call the secondView I only get a white screen, I posted the code of my secondViewController above. – Informatics Feb 13 '18 at 10:13
  • @Informatics You shouldn’t reassign `self.view` in `viewDidLoad()`. If you instantiate `InformationViewController` from storyboard and it has `secondView` there, the `view` will contain it. – Lësha Turkowski Feb 13 '18 at 10:47
  • Ok, I changed it, but I have the problem, that I only get a white screen without content. – Informatics Feb 13 '18 at 13:22
  • @Informatics do you initialize view controller using UIStoryboard? – Lësha Turkowski Feb 13 '18 at 13:36
  • Yes: guard let secondViewController = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController else { print ("No secondController") return} – Informatics Feb 13 '18 at 14:51
  • 2
    This worked for me! Thank you so much! By the way, I also had the problem of only seeing a white plane, but that was because I wasn't storing a reference to the view controller anywhere. I declared a variable inside the main view controller to keep a reference to the secondary view controller and now it works, it even scrolls (I used a table view controller to test the interaction). – Oscar A. Esquivel Oviedo Apr 04 '18 at 18:09
  • Please can you explain with more details because I created let vc = SecondVC() In the main controller not inside the method but only I see a white screen – Fabio Sep 28 '18 at 03:56
  • that is an extremely interesting observation.. however for me it doesn't seem that UITapGestureRecognizers inside the view work, do the? also normal touches seem to be ignored.. any idea? .-. – thisIsTheFoxe Mar 20 '19 at 16:00
  • @LëshaTurkowski This doesn't seem to work anymore. Read here: https://stackoverflow.com/questions/59446393/arkit-uiview-setanimationsenabled-performing-any-operation-from-a-background and here: https://forums.raywenderlich.com/t/in-chapter-11-i-get-an-error-when-a-qrcode-is-detected/76973 – Lance Samaria Jan 22 '20 at 12:09
  • it works perfectly https://stackoverflow.com/a/74380559/294884 You must use the LAYER though. it's that easy. – Fattie Nov 09 '22 at 19:55
2

I cannot provide you code now but this is how to do it.

  1. Create a SCNPlane.
  2. Create your UIView with all elements you need.
  3. Create image context from UIView.
  4. Use this image as material for SCNPlane.

Or even easier make SKScene with label and add it as material for SCNPlane.

Example: https://stackoverflow.com/a/74380559/294884

Fattie
  • 27,874
  • 70
  • 431
  • 719
Alok Subedi
  • 1,601
  • 14
  • 26
  • Thanks a lot! How can I create an image context from UIView? – Informatics Feb 09 '18 at 08:49
  • I am sorry i cant give you anything now. You can find creating image from uiview easily on stackoverflow – Alok Subedi Feb 09 '18 at 09:20
  • Thanks a lot! My problem is now that I cant call the view. It doesn´t work with the ViewController. – Informatics Feb 13 '18 at 08:13
  • Have you used/watched twitter ar app? This method is similar (thats what i think). You can just display UIView as material for plane. And have other nodes(plane) as buttons. You can change UIView's elements with tap on nodes used as buttons and change the material for plane. – Alok Subedi Feb 13 '18 at 16:37
  • And I think Lësha Turkowski's method is what you need. – Alok Subedi Feb 13 '18 at 16:38
  • @AlokSubedi when I created UIView to image and applied but button clicks not working. how to get event for inside the UIView ? – Yogendra Girase Feb 21 '19 at 09:29
  • @YogendraGirase button will not work because it is just an image not a functional UIView. You can just pass UIView with button instead of converting it to image like Lesha's answer, but it was not stable in my case. Seems it is still buggy. So what I did was add a UIView on SCNNode as image and used hittest to fake button tapped. – Alok Subedi Feb 25 '19 at 06:46
1

To place text in a label in the world you draw it into an image and then attach that image to a SCNNode.

For example:

let text = "Hello, Stack Overflow."
let font = UIFont(name: "Arial", size: CGFloat(size))
let width = 128
let height = 128

let fontAttrs: [NSAttributedStringKey: Any] = 
[NSAttributedStringKey.font: font as UIFont]

let stringSize = self.text.size(withAttributes: fontAttrs)
let rect = CGRect(x: CGFloat((width / 2.0) - (stringSize.width/2.0)),
                  y: CGFloat((height / 2.0) - (stringSize.height/2.0)),
                  width: CGFloat(stringSize.width),
                  height: CGFloat(stringSize.height))

let renderer = UIGraphicsImageRenderer(size: CGSize(width: CGFloat(width), height: CGFloat(height)))
let image = renderer.image { context in

    let color = UIColor.blue.withAlphaComponent(CGFloat(0.5))

    color.setFill()
    context.fill(rect)

    text.draw(with: rect, options: .usesLineFragmentOrigin, attributes: fontAttrs, context: nil)
}

let plane = SCNPlane(width: CGFloat(0.1), height: CGFloat(0.1))
plane.firstMaterial?.diffuse.contents = image

let node = SCNNode(geometry: plane)

EDIT: I added these lines:

let color = UIColor.blue.withAlphaComponent(CGFloat(0.5))

color.setFill()
context.fill(rect)

This lets you set the background color and the opacity. There are other ways of doing this - which also let you draw complex shapes - but this is the easiest for basic color.

EDIT 2: Added reference to stringSize and rect

Jordan
  • 481
  • 4
  • 14
  • Thanks a lot! It works fine!!! Do you know, how I can change the background color and the opacity? – Informatics Feb 13 '18 at 08:11
  • Yep - I just added in the three lines you will need in order to draw the background color. – Jordan Feb 14 '18 at 19:17
  • Hey there, where does your code get the `rect` variable from? – Cesare Dec 16 '18 at 10:27
  • @Cesare I added this in now. The `rect` is the bounding box that you want to draw into within the image. `x`, `y` is the top left corner. – Jordan Dec 20 '18 at 05:11
  • @Jordan thanks, this code is not displaying anything on my end. – Cesare Dec 23 '18 at 09:26
  • @Jordan when I created UIView to image and applied but button clicks not working. how to get event for inside the UIView ? – Yogendra Girase Feb 21 '19 at 09:33
  • This method turns the view into an image for display. This is the most robust if you want it to look correct - but you lose all the functionality of the view (so you won't be able to tap on internal buttons). If you still want it to be an actual UIView you will need one of the answers provided above, i.e. https://stackoverflow.com/a/48712156/7098234 – Jordan Feb 21 '19 at 19:08