Building a Portal App in ARKit: Adding Objects

In this second part of our tutorial series on building a portal app in ARKit, you’ll build up your app and add 3D virtual content to the camera scene via SceneKit. By Namrata Bandekar.

Leave a rating/review
Download materials
Save for later
You are currently viewing page 2 of 3 of this article. Click here to view the first page.


Hide contents

Adding Crosshairs

Before you add the portal to the scene, there is one last thing you need to add in the view. In the previous section, you implemented detecting hit testing for sceneView with the center of the device screen. In this section, you’ll work on adding a view to display the screen’s center so as to help the user position the device.

Open Main.storyboard. Navigate to the Object Library and search for a View object. Drag and drop the view object onto the PortalViewController.

Change the name of the view to Crosshair. Add layout constraints to the view such that its center matches its superview’s centre. Add constraints to set the width and height of the view to 10. In the Size Inspector tab, your constraints should look like this:

Navigate to the Attributes inspector tab and change the background color of the Crosshair view to Light Gray Color.

Select the assistant editor and you’ll see PortalViewController.swift on the right. Press Ctrl and drag from the Crosshair view in storyboard to the PortalViewController code, just above the declaration for sceneView.

Enter crosshair for the name of the IBOutlet and click Connect.

Build and run the app. Notice there’s a gray square view at the center of the screen. This is the crosshair view that you just added.

Now add the following code to the ARSCNViewDelegate extension of the PortalViewController.

// 1
func renderer(_ renderer: SCNSceneRenderer,
              updateAtTime time: TimeInterval) {
  // 2
  DispatchQueue.main.async {
    // 3
    if let _ = self.sceneView?.hitTest(self.viewCenter,
      types: [.existingPlaneUsingExtent]).first {
      self.crosshair.backgroundColor =
    } else { // 4
      self.crosshair.backgroundColor = UIColor.lightGray

Here’s what’s happening with the code you just added:

  1. This method is part of the SCNSceneRendererDelegate protocol which is implemented by the ARSCNViewDelegate. It contains callbacks which can be used to perform operations at various times during the rendering. renderer(_: updateAtTime:) is called exactly once per frame and should be used to perform any per-frame logic.
  2. You run the code to detect if the screen’s center falls in the existing detected horizontal planes and update the UI accordingly on the main queue.
  3. This performs a hit test on the sceneView with the viewCenter to determine if the view center indeed intersects with a horizontal plane. If there’s at least one result detected, the crosshair view’s background color is changed to green.
  4. If the hit test does not return any results, the crosshair view’s background color is reset to light gray.

Build and run the app.

Move the device around so that it detects and renders a horizontal plane, as shown on the left. Now move the device such that the device screen’s center falls within the plane, as shown on the right. Notice that the center view’s color changes to green.

Adding a State Machine

Now that you have set up the app for detecting planes and placing an ARAnchor, you can get started with adding the portal.

To track the state your app, add the following variables to PortalViewController:

var portalNode: SCNNode? = nil
var isPortalPlaced = false

You store the SCNNode object that represents your portal in portalNode and use isPortalPlaced to keep state of whether the portal is rendered in the scene.

Add the following method to PortalViewController:

func makePortal() -> SCNNode {
  // 1
  let portal = SCNNode()
  // 2
  let box = SCNBox(width: 1.0,
                   height: 1.0,
                   length: 1.0,
                   chamferRadius: 0)
  let boxNode = SCNNode(geometry: box)
  // 3
  return portal

Here you define makePortal(), a method that will configure and render the portal. There are a few things happening here:

  1. You create an SCNNode object which will represent your portal.
  2. This initializes a SCNBox object which is a cube and makes a SCNNode object for the box using the SCNBox geometry.
  3. You add the boxNode as a child node to your portal and return the portal node.

Here, makePortal() is creating a portal node with a box object inside it as a placeholder.

Now replace the renderer(_:, didAdd:, for:) and renderer(_:, didUpdate:, for:) methods for the SCNSceneRendererDelegate with the following:

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
  DispatchQueue.main.async {
    // 1
    if let planeAnchor = anchor as? ARPlaneAnchor, 
    !self.isPortalPlaced {
      #if DEBUG
        let debugPlaneNode = createPlaneNode(
          extent: planeAnchor.extent)
      self.messageLabel?.alpha = 1.0
      self.messageLabel?.text = """
            Tap on the detected \
            horizontal plane to place the portal
    else if !self.isPortalPlaced {// 2
        // 3
      self.portalNode = self.makePortal()
      if let portal = self.portalNode {
        // 4
        self.isPortalPlaced = true

        // 5
        self.sceneView?.debugOptions = []

        // 6
        DispatchQueue.main.async {
          self.messageLabel?.text = ""
          self.messageLabel?.alpha = 0


func renderer(_ renderer: SCNSceneRenderer,
              didUpdate node: SCNNode,
              for anchor: ARAnchor) {
  DispatchQueue.main.async {
    // 7
    if let planeAnchor = anchor as? ARPlaneAnchor,
      node.childNodes.count > 0,
      !self.isPortalPlaced {
                      extent: planeAnchor.extent)

Here are the changes you made:

  1. You’re adding a horizontal plane to the scene to show the detected planes only if the anchor that was added to the scene is an ARPlaneAnchor, and only if isPortalPlaced equals false, which means the portal has not yet been placed.
  2. If the anchor that was added was not an ARPlaneAnchor, and the portal node still hasn’t been placed, this must be the anchor you add when the user taps on the screen to place the portal.
  3. You create the portal node by calling makePortal().
  4. renderer(_:, didAdd:, for:) is called with the SCNNode object, node, that is added to the scene. You want to place the portal node at the location of the node. So you add the portal node as a child node of node and you set isPortalPlaced to true to track that the portal node has been added.
  1. To clean up the scene, you remove all rendered horizontal planes and reset the debugOptions for sceneView so that the feature points are no longer rendered on screen.
  2. You update the messageLabel on the main thread to reset its text and hide it.
  3. In the renderer(_:, didUpdate:, for:) you update the rendered horizontal plane only if the given anchor is an ARPlaneAnchor, if the node has at least one child node and if the portal hasn’t been placed yet.

Finally, replace removeAllNodes() with the following.

func removeAllNodes() {
  // 1
  // 2
  // 3
  self.isPortalPlaced = false

This method is used for cleanup and removing all rendered objects from the scene. Here’s a closer look at what’s happening:

  1. You remove all the rendered horizontal planes.
  2. You then remove the portalNode from its parent node.
  3. Change the isPortalPlaced variable to false to reset the state.

Build and run the app; let the app detect a horizontal plane and then tap on the screen when the crosshair view turns green. You will see a rather plain-looking, huge white box.

This is the placeholder for your portal. In the next and final part of this tutorial series, you’ll add some walls and a doorway to the portal. You’ll also add textures to the walls so that they look more realistic.