Code a Measuring App With ARKit: Interacting and Measuring

Along with many other things which have quickly been replaced by our modern technology, it looks as if the common tape measure may be the next to go. In this two-part tutorial series, we’re learning how to use augmented reality and the camera on your iOS device to create an app which will report the distance between two points.

In the first post, we created the app project and coded its main interface elements. In this post, we’ll finish it off by measuring between two points in the AR scene. If you haven’t yet, follow along with the first post to get your ARKit project set up.

Handling Taps

Here’s one of the biggest parts of this tutorial: handling when the user taps on their world to get a sphere to appear exactly where they tapped. Later, we’ll calculate the distance between these spheres to finally show the user their distance.

Tap Gesture Recognizer

The first step in checking for taps is to create a tap gesture recognizer when the app launches. To do this, create a tap handler as follows:

// Creates a tap handler and then sets it to a constant
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))

The first line creates an instance of the UITapGestureRecognizer() class and passes in two parameters at the initialization: the target and the action. The target is the recipient of the notifications which this recognizer sends, and we want our ViewController class to be the target. The action is simply a method which should be called each time there is a tap.

To set the number of taps, add this:

// Sets the amount of taps needed to trigger the handler
tapRecognizer.numberOfTapsRequired = 1

Next, the instance of the class we created earlier needs to know how many taps are actually needed to activate the recognizer. In our case, we just need one tap, but in other apps, you may need to have more (such as a double tap) for some cases.

Add the handler to the scene view like this:

// Adds the handler to the scene view
sceneView.addGestureRecognizer(tapRecognizer)

Lastly, this single line of code just adds the gesture recognizer to the sceneView, which is where we’ll be doing everything. This is where the preview of the camera will be as well as what the user will directly tap in order to get a sphere to appear on the screen, so it makes sense to add the recognizer to the view with which the user will interact.

Handle Tap Method

When we created the UITapGestureRecognizer(), you may remember that we set a handleTap method to the action. Now, we’re ready to declare that method. To do this, simply add the following to your app:

@objc func handleTap(sender: UITapGestureRecognizer) {
    // Your code goes here
}

Though the function declaration may be pretty self-explanatory, you may wonder why there is an @objc tag in front of it. As of the current version of Swift, to expose methods to Objective-C, you need this tag. All you need to know is that #selector needs the referred method to be available to Objective-C. Lastly, the method parameter will let us get the exact location which was tapped on the screen.

Location Detection

The next step in getting our spheres to appear where the user tapped is to detect the exact position which they tapped. Now, this isn’t as simple as getting the location and placing a sphere, but I am sure that you’ll master it in no time. 

Start by adding the following three lines of code to your handleTap() method:

// Gets the location of the tap and assigns it to a constant
let location = sender.location(in: sceneView)

// Searches for real world objects such as surfaces and filters out flat surfaces
let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])

// Assigns the most accurate result to a constant if it is non-nil
guard let result = hitTest.last else { return }

If you remember the parameter we took in the handleTap() method, you may recall that it was named sender, and it was of type UITapGestureRecognizer. Well, this first line of code simply takes the location of the tap on the screen (relative to the scene view), and sets it to a constant named location.

Next, we’re doing something called a hit test on the SceneView itself. What this does, in simple terms, is to check the scene for real objects, such as tables, surfaces, walls, floors, etc. This allows us to get a sense of depth and to get pretty accurate measurements between two points. In addition, we’re specifying the types of objects to detect, and as you can see, we’re telling it to look for featurePoints, which are essentially flat surfaces, which makes sense for a measuring app.

Lastly, the line of code takes the most accurate result, which in the case of hitTest is the last result, and checks if it isn’t nil. If it is, it ignores the rest of the lines in this method, but if there is indeed a result, it will be assigned to a constant called result.

Matrices

If you think back to your high-school algebra class, you may remember matrices, which might not have seemed as important back then as they are right now. They’re commonly used in computer graphics related tasks, and we’ll be getting a glimpse of them in this app.

Add the following lines to your handleTap() method, and we’ll go over them in detail:

// Converts the matrix_float4x4 to an SCNMatrix4 to be used with SceneKit
let transform = SCNMatrix4.init(result.worldTransform)

// Creates an SCNVector3 with certain indexes in the matrix
let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)

// Makes a new sphere with the created method
let sphere = newSphere(at: vector)

Before getting into the first line of code, it’s important to understand that the hit test we did earlier returns a type of matrix_float4x4, which is essentially a four-by-four matrix of float values. Since we are in SceneKit, though, we’ll need to convert it into something that  SceneKit can understand—in this case, to a SCNMatrix4.

Then, we’ll use this matrix to create a SCNVector3, which, as its name suggests, is a vector with three components. As you may have guessed, those components are xy, and z, to give us a position in space. transform.m41transform.m42, and transform.m43 are the relevant coordinate values for the three component vectors.

Lastly, let’s use the newSphere() method that we created earlier, along with the location information we parsed from the touch event, to make a sphere and assign it to a constant called sphere.

Solving the Double-Tap Bug

Now, you may have realized a slight flaw in our code; if the user keeps tapping, a new sphere would keep getting created. We don’t want this because it makes it hard to determine which spheres need to be measured. Also, it’s difficult for the user to keep track of all the spheres!

Solving With Arrays

The first step to solve this is to create an array at the top of the class.

var spheres: [SCNNode] = []

This is an array of SCNNodes because that’s the type that we returned from our newSphere() method that we created towards the beginning of this tutorial. Later on, we’ll put the spheres in this array and check how many there are. Based on that, we’ll be able to manipulate their numbers by removing and adding them.

Optional Binding

Next, we’ll use a series of if-else statements and for loops to figure out if there are any spheres in the array or not. For starters, add the following optional binding to your app:

if let first = spheres.first {
    // Your code goes here
} else {
    // Your code goes here
}

First, we’re checking if there are any items in the spheres array, and if not, execute the code in the else clause.

Auditing the Spheres

After that, add the following to the first part (the if branch) of your if-else statement:

// Adds a second sphere to the array
spheres.append(sphere)
print(sphere.distance(to: first))

// If more that two are present...
if spheres.count > 2 {
    
    // Iterate through spheres array
    for sphere in spheres {
        
        // Remove all spheres
        sphere.removeFromParentNode()
    }
    
    // Remove extraneous spheres
    spheres = [spheres[2]]
}

Since we’re already in a tap event, we know that we’re creating another sphere. So if there is already one sphere, we need to get the distance and display it to the user. You can call the distance() method on the sphere, because later, we’ll create an extension of SCNNode.

Next, we need to know if there are already more than the maximum of two spheres. To do this, we’re just using the count property of our spheres array and an if statement. We iterate through all of the spheres in the array and remove them from the scene. (Don’t worry, we’ll some of them back later.)

Finally, since we’re already in the if statement which tells us that there are more than two spheres, we can remove the third one in the array so that we ensure that only two are left in the array at all times.

Adding the Spheres

Finally, in the else clause, we know that the spheres array is empty, so what we need to do is to just add the sphere that we created at the time of the method call. Inside your else clause, add this:

// Add the sphere
spheres.append(sphere)

Yay! We just added the sphere to our spheres array, and our array is ready for the next tap. We’ve now prepared our array with the spheres that should be on the screen, so now, let’s just add these to the array.

In order to iterate through and add the spheres, add this code:

// Iterate through spheres array
for sphere in spheres {
    
    // Add all spheres in the array
    self.sceneView.scene.rootNode.addChildNode(sphere)
}

This is just a simple for loop, and we’re adding the spheres (SCNNode) as a child of the scene’s root node. In SceneKit, this is the preferred way to add things.

Full Method

Here’s what the final handleTap() method should look like:

@objc func handleTap(sender: UITapGestureRecognizer) {
    
    let location = sender.location(in: sceneView)
    let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
    
    guard let result = hitTest.last else { return }
    
    let transform = SCNMatrix4.init(result.worldTransform)
    let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)
    let sphere = newSphere(at: vector)
    
    if let first = spheres.first {
        spheres.append(sphere)
        print(sphere.distance(to: first))
        
        if spheres.count > 2 {
            for sphere in spheres {
                sphere.removeFromParentNode()
            }
            
            spheres = [spheres[2]]
        }
    
    } else {
        spheres.append(sphere)
    }
    
    for sphere in spheres {
        self.sceneView.scene.rootNode.addChildNode(sphere)
    }
}

Calculating Distances

Now, if you’ll remember, we called a distance(to:) method on our SCNNode, the sphere, and I’m sure that Xcode is yelling at you for using an undeclared method. Let’s end that now, by creating an extension of the SCNNode class.

To create an extension, just do the following outside of your ViewController class:

extension SCNNode {
    // Your code goes here
}

This simply lets you alter the class (it’s as if you were editing the actual class). Then, we’ll add a method which will compute the distance between two nodes.

Here’s the function declaration to do that:

func distance(to destination: SCNNode) -> CGFloat {
    // Your code goes here
}

If you’ll see, there’s a parameter which is another SCNNode, and it returns a CGFloat as the result. For the actual calculation, add this to your distance() function:

let dx = destination.position.x - position.x
let dy = destination.position.y - position.y
let dz = destination.position.z - position.z

let inches: Float = 39.3701
let meters = sqrt(dx*dx + dy*dy + dz*dz)

return CGFloat(meters * inches)

The first three lines of code subtract the x, y, and z positions of the current SCNNode from the coordinates of the node passed as a parameter. We’ll later plug these values into the distance formula to get their distance. Also, because I want the result in inches, I’ve created a constant for the conversion rate between meters and inches for easy conversion later on. 

Now, to get the distance between the two nodes, think back to your middle-school math class: you may remember the distance formula for the Cartesian plane. Here, we’re applying it to points in three-dimensional space.

Finally, we return the value multiplied by the inches conversion ratio to get the appropriate unit of measure. If you live outside the United States, you can leave it in meters or convert it to centimeters if you wish.

Conclusion

Well, that’s a wrap! Here’s what your final project should look like:

Final result showing the measuring app in action

As you can see, the measurements aren’t perfect, but it thinks a 15-inch computer is around 14.998 inches, so it’s not bad!

You now know how to measure distances using Apple’s new library, ARKit. This app can be used for many things, and I challenge you to think of different ways that this can be used in the real world, and be sure to leave your thoughts in the comments below.

Also, be sure to check out the GitHub repo for this app. And while you’re still here, check out our other iOS development tutorials here on Envato Tuts+!

Leave a Reply

Your email address will not be published. Required fields are marked *