© Wallace Wang 2018
Wallace WangBeginning ARKit for iPhone and iPadhttps://doi.org/10.1007/978-1-4842-4102-8_17

17. Persistence

Wallace Wang1 
(1)
San Diego, CA, USA
 

Up until now, every augmented reality view project we’ve created has had one problem. While you might be able to add virtual objects to an augmented reality view, the moment you close the app and start it again, any virtual objects you added would now be gone.

In many cases, this is exactly what you want, so starting the app again creates a blank augmented reality view for the user. However, sometimes you may want to retain any virtual objects placed in the view. To save an augmented reality view so it appears in another person’s app or in your own app if you start it up again at a later time, you need to use persistence.

Persistence simply saves any virtual objects placed in an augmented reality view in a world map. By saving this world map, you can retain the placement of virtual objects. By loading this world map later, you can restore an augmented reality view to a previous state.

To learn about persistence, let’s create a new Xcode project by following these steps:
  1. 1.

    Start Xcode. (Make sure you’re using Xcode 10 or greater.)

     
  2. 2.

    Choose File ➤ New ➤ Project. Xcode asks you to choose a template.

     
  3. 3.

    Click the iOS category.

     
  4. 4.

    Click the Single View App icon and click the Next button. Xcode asks for a product name, organization name, organization identifiers, and content technology.

     
  5. 5.

    Click in the Product Name text field and type a descriptive name for your project, such as Persistence. (The exact name does not matter.)

     
  6. 6.

    Click the Next button. Xcode asks where you want to store your project.

     
  7. 7.

    Choose a folder and click the Create button. Xcode creates an iOS project.

     
Now modify the Info.plist file to allow access to the camera and to use ARKit by following these steps:
  1. 1.

    Click the Info.plist file in the Navigator pane. Xcode displays a list of keys, types, and values.

     
  2. 2.

    Click the disclosure triangle to expand the Required Device Capabilities category to display Item 0.

     
  3. 3.

    Move the mouse pointer over Item 0 to display a plus (+) icon.

     
  4. 4.

    Click this plus (+) icon to display a blank Item 1.

     
  5. 5.

    Type arkit under the Value category in the Item 1 row.

     
  6. 6.

    Move the mouse pointer over the last row to display a plus (+) icon.

     
  7. 7.

    Click on the plus (+) icon to create a new row. A popup menu appears.

     
  8. 8.

    Choose Privacy – Camera Usage Description.

     
  9. 9.

    Type AR needs to use the camera under the Value category in the Privacy – Camera Usage Description row.

     
Now it’s time to modify the ViewController.swift file to use ARKit and SceneKit by following these steps:
  1. 1.

    Click on the ViewController.swift file in the Navigator pane.

     
  2. 2.

    Edit the ViewController.swift file so it looks like this:

     
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate , ARSessionDelegate  {
let configuration = ARWorldTrackingConfiguration()
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
}

The most important line to notice is the one that adds the ARSCNViewDelegate because this contains the functions we need to save and restore a world map.

To view augmented reality in our app, add the following to the Main.storyboard, as shown in Figure 17-1:
  • A single ARKit SceneKit view (ARSCNView)

  • Three UIButtons

  • A single UILabel

../images/469983_1_En_17_Chapter/469983_1_En_17_Fig1_HTML.jpg
Figure 17-1

Three buttons, a label, and an ARKit SceneKit view on the user interface

After you’ve designed your user interface, you need to add constraints. To add constraints, choose Editor ➤ Resolve Auto Layout Issues ➤ Reset to Suggested Constraints at the bottom half of the menu under the All Views in Container category.

The next step is to connect the user interface items to the Swift code in the ViewController.swift file. To do this, follow these steps:
  1. 1.

    Click the Main.storyboard file in the Navigator pane.

     
  2. 2.

    Click the Assistant Editor icon or choose View ➤ Assistant Editor ➤ Show Assistant Editor to display the Main.storyboard and the ViewController.swift file side by side.

     
  3. 3.

    Move the mouse pointer over the ARSCNView, hold down the Control key, and Ctrl-drag under the class ViewController line.

     
  4. 4.

    Release the Control key and the left mouse button. A popup menu appears.

     
  5. 5.
    Click in the Name text field and type sceneView, then click the Connect button. Xcode creates an IBOutlet as shown here:
    @IBOutlet var sceneView: ARSCNView!
     
  6. 6.

    Move the mouse over the label, hold down the Control key, and Ctrl-drag under the IBOutlet you just created.

     
  7. 7.

    Release the Control key and the left mouse button. A popup menu appears.

     
  8. 8.
    Click in the Name text field and type lblMessage, then click the Connect button. Xcode creates an IBOutlet as shown here:
    @IBOutlet var lblMessage: UILabel!
     
  9. 9.
    Underneath the two IBOutlets, type the following:
    let configuration = ARWorldTrackingConfiguration()
     
  10. 10.
    Edit the viewDidLoad function so it looks like this:
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
            sceneView.debugOptions = [ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints]
            sceneView.delegate = self
            sceneView.session.delegate = self
            configuration.planeDetection = .horizontal
            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
            sceneView.addGestureRecognizer(tapGesture)
            self.lblMessage.text = "Tap to place a virtual object"
            sceneView.session.run(configuration)
        }
     
At this point, we defined a tap gesture but we need to create a function to handle this tap gesture. Underneath the viewDidLoad function , add the following function called handleTap :
    @objc func handleTap(sender: UITapGestureRecognizer) {
        guard let sceneView = sender.view as? ARSCNView else {
            return
        }
        let touch = sender.location(in: sceneView)
        let hitTestResults = sceneView.hitTest(touch, types: [.featurePoint, .estimatedHorizontalPlane])
        if hitTestResults.isEmpty == false {
            if let hitTestResult = hitTestResults.first {
                let virtualAnchor = ARAnchor(transform: hitTestResult.worldTransform)
                self.sceneView.session.add(anchor: virtualAnchor)
            }
        }
    }
Each time the user taps on the screen, it adds an ARAnchor to the augmented reality view. That also triggers the didAdd renderer function, which we’ll need to write to display a blue box on the screen. Underneath the handleTap function, write the didAdd renderer function as follows:
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        if anchor is ARPlaneAnchor {
            return
        }
        let newNode = SCNNode(geometry: SCNBox(width: 0.05, height: 0.05, length: 0.05, chamferRadius: 0))
        newNode.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
        node.addChildNode(newNode)
    }
Finally, add a viewWillDisappear function as follows:
    override func viewWillDisappear(_ animated: Bool) {
        sceneView.session.pause()
    }

Saving a World Map

At this point, the app will simply allow the user to tap the screen and place blue boxes in the augmented reality view. However, you cannot save the augmented reality view yet. To save an augmented reality view, we need to store the current augmented reality view as a world map.

We can save the world map to any database, but since the world map represents a small amount of data, we’re going to store it in the User Defaults database, which is typically used to store app settings. To save a world map, follow these steps:
  1. 1.

    Click the Main.storyboard file in the Navigator pane.

     
  2. 2.

    Click the Assistant Editor icon or choose View ➤ Assistant Editor ➤ Show Assistant Editor to display the Main.storyboard and the ViewController.swift file side by side.

     
  3. 3.

    Move the mouse pointer over the button displaying Save on the user interface, hold down the Control key, and Ctrl-drag under the class ViewController line.

     
  4. 4.

    Release the Control key and the left mouse button. A popup menu appears.

     
  5. 5.

    Make sure the Connection popup menu displays Action, then click in the Name text field and type saveButton.

     
  6. 6.
    Click in the Type popup menu and choose UIButton, then click the Connect button. Xcode creates an IBAction method as shown here:
        @IBAction func saveButton(_ sender: UIButton) {
        }
     
  7. 7.
    Edit this saveButton IBAction method as follows:
        @IBAction func saveButton(_ sender: UIButton) {
            saveMap()
        }
     
Each time the user taps the Save button, the Save button runs the saveMap() function , which we’ll need to write as follows:
    func saveMap() {
    }
The first step to saving the world map is to get the current state of the augmented reality view:
        self.sceneView.session.getCurrentWorldMap { worldMap, error in
        }
This code either retrieves the current state (worldMap) or shows an error. In case there’s an error saving the current augmented reality state, we need to display an error message and stop trying to save the world map:
   if error != nil {
       print(error?.localizedDescription ?? "Unknown error")
       return
   }
If we’re successful in retrieving the current state (worldMap), then we can create a “map” variable to represent our current world map:
   if let map = worldMap {
   }
Next we need to archive this world map as follows:
let data = try! NSKeyedArchiver.archivedData(withRootObject: map, requiringSecureCoding: true)
Now we need to save this data in the User Defaults database and give it an arbitrary string as a key so we can retrieve it later:
   let savedMap = UserDefaults.standard
   savedMap.set(data, forKey: "worldmap")
   savedMap.synchronize()
Finally, we need to send a message to the user that we saved the world map:
   DispatchQueue.main.async {
      self.lblMessage.text = "World map saved"
   }
The entire saveMap function should look like this:
    func saveMap() {
        self.sceneView.session.getCurrentWorldMap { worldMap, error in
            if error != nil {
                print(error?.localizedDescription ?? "Unknown error")
                return
            }
            if let map = worldMap {
                let data = try! NSKeyedArchiver.archivedData(withRootObject: map, requiringSecureCoding: true)
                // save in user defaults
                let savedMap = UserDefaults.standard
                savedMap.set(data, forKey: "worldmap")
                savedMap.synchronize()
                DispatchQueue.main.async {
                    self.lblMessage.text = "World map saved"
                }
            }
        }
    }

Loading a World Map

After we’ve saved a world map, the next step is to load that world map back into the augmented reality view. This requires using the user defaults key (defined as worldmap). To retrieve the stored world map, we need to connect the Load button on the user interface to an IBAction method and then write code to retrieve any data stored in the user defaults database.

To load a world map, follow these steps:
  1. 1.

    Click the Main.storyboard file in the Navigator pane.

     
  2. 2.

    Click the Assistant Editor icon or choose View ➤ Assistant Editor ➤ Show Assistant Editor to display the Main.storyboard and the ViewController.swift file side by side.

     
  3. 3.

    Move the mouse pointer over the button displaying Load on the user interface, hold down the Control key, and Ctrl-drag under the class ViewController line.

     
  4. 4.

    Release the Control key and the left mouse button. A popup menu appears.

     
  5. 5.

    Make sure the Connection popup menu displays Action, then click in the Name text field and type saveButton.

     
  6. 6.
    Click in the Type popup menu and choose UIButton, then click the Connect button. Xcode creates an IBAction method as shown here:
        @IBAction func loadButton(_ sender: UIButton) {
        }
     
  7. 7.
    Edit this saveButton IBAction method as follows:
        @IBAction func loadButton(_ sender: UIButton) {
            loadMap()
        }
     

Each time the user taps the Load button, the Load button runs the loadMap() function , which will need to either retrieve a previously saved world map or simply start an ordinary augmented reality session in case no previous world map has been stored.

First, create the loadMap function like this:
func loadMap() {
    }
Now we need to retrieve the data stored in the user defaults database:
let storedData = UserDefaults.standard
Next, we need an if-else statement where if a world map is found (using the arbitrary worldmap key), one set of code runs, and if a world map is not found, a second set of code runs:
        if let data = storedData.data(forKey: "worldmap") {
        } else {
        }
If a worldmap key is found, then we need to retrieve and unarchive it:
if let unarchived = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [ARWorldMap.classForKeyedUnarchiver()], from: data), let worldMap = unarchived as? ARWorldMap {
}
Then we can store the previously saved world map into the initialWorldMap property and display a message to the user that the world map has been loaded. Finally we can run that configuration:
     let configuration = ARWorldTrackingConfiguration()
     configuration.initialWorldMap = worldMap
     configuration.planeDetection = .horizontal
     self.lblMessage.text = "Previous world map loaded"
     sceneView.session.run(configuration)
If a world map has not been found using our arbitrary worldmap key, we just need to load a regular configuration for the augmented reality view so the entire loadMap function looks like this:
    func loadMap() {
        let storedData = UserDefaults.standard
        if let data = storedData.data(forKey: "worldmap") {
            if let unarchived = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [ARWorldMap.classForKeyedUnarchiver()], from: data), let worldMap = unarchived as? ARWorldMap {
                let configuration = ARWorldTrackingConfiguration()
                configuration.initialWorldMap = worldMap
                configuration.planeDetection = .horizontal
                self.lblMessage.text = "Previous world map loaded"
                sceneView.session.run(configuration)
            }
        } else {
            let configuration = ARWorldTrackingConfiguration()
            configuration.planeDetection = .horizontal
            sceneView.session.run(configuration)
        }
    }

Clearing an Augmented Reality View

At this point, our app can save a world map and load it again, but let’s make one final adjustment and create a Clear button. When the user taps the Clear button, we need to remove any virtual objects so we can create and save a new augmented reality view.

To clear an augmented reality view, follow these steps:
  1. 1.

    Click the Main.storyboard file in the Navigator pane.

     
  2. 2.

    Click the Assistant Editor icon or choose View ➤ Assistant Editor ➤ Show Assistant Editor to display the Main.storyboard and the ViewController.swift file side by side.

     
  3. 3.

    Move the mouse pointer over the button displaying Clear on the user interface, hold down the Control key, and Ctrl-drag under the class ViewController line.

     
  4. 4.

    Release the Control key and the left mouse button. A popup menu appears.

     
  5. 5.

    Make sure the Connection popup menu displays Action, then click in the Name text field and type saveButton.

     
  6. 6.
    Click in the Type popup menu and choose UIButton, then click the Connect button. Xcode creates an IBAction method as shown here:
        @IBAction func clearButton(_ sender: UIButton) {
        }
     
  7. 7.
    Edit this saveButton IBAction method as follows:
        @IBAction func clearButton(_ sender: UIButton) {
            clearMap()
        }
     
Each time the user taps the Clear button, the Clear button runs the clearMap() function, which resets tracking and removes any existing anchors where planes and virtual objects exist, essentially clearing the augmented reality view. Add the following clearMap function as follows:
    func clearMap() {
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = .horizontal
        self.lblMessage.text = "Tap to place a virtual object"
        sceneView.debugOptions = [.showWorldOrigin, .showFeaturePoints]
        let options: ARSession.RunOptions = [.resetTracking, .removeExistingAnchors]
        sceneView.session.run(configuration, options: options)
    }
The entire ViewController.swift file should look like this:
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate , ARSessionDelegate {
    @IBOutlet var sceneView: ARSCNView!
    @IBOutlet var lblMessage: UILabel!
    let configuration = ARWorldTrackingConfiguration()
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        sceneView.debugOptions = [ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints]
        sceneView.delegate = self
        sceneView.session.delegate = self
        configuration.planeDetection = .horizontal
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
        sceneView.addGestureRecognizer(tapGesture)
        self.lblMessage.text = "Tap to place a virtual object"
        sceneView.session.run(configuration)
    }
    @objc func handleTap(sender: UITapGestureRecognizer) {
        guard let sceneView = sender.view as? ARSCNView else {
            return
        }
        let touch = sender.location(in: sceneView)
        let hitTestResults = sceneView.hitTest(touch, types: [.featurePoint, .estimatedHorizontalPlane])
        if hitTestResults.isEmpty == false {
            if let hitTestResult = hitTestResults.first {
                let virtualAnchor = ARAnchor(transform: hitTestResult.worldTransform)
                self.sceneView.session.add(anchor: virtualAnchor)
            }
        }
    }
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        if anchor is ARPlaneAnchor {
            return
        }
        let newNode = SCNNode(geometry: SCNBox(width: 0.05, height: 0.05, length: 0.05, chamferRadius: 0))
        newNode.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
        node.addChildNode(newNode)
    }
    func saveMap() {
        self.sceneView.session.getCurrentWorldMap { worldMap, error in
            if error != nil {
                print(error?.localizedDescription ?? "Unknown error")
                return
            }
            if let map = worldMap {
                let data = try! NSKeyedArchiver.archivedData(withRootObject: map, requiringSecureCoding: true)
                // save in user defaults
                let savedMap = UserDefaults.standard
                savedMap.set(data, forKey: "worldmap")
                savedMap.synchronize()
                DispatchQueue.main.async {
                    self.lblMessage.text = "World map saved"
                }
            }
        }
    }
    override func viewWillDisappear(_ animated: Bool) {
        sceneView.session.pause()
    }
    func loadMap() {
        let storedData = UserDefaults.standard
        if let data = storedData.data(forKey: "worldmap") {
            if let unarchived = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [ARWorldMap.classForKeyedUnarchiver()], from: data), let worldMap = unarchived as? ARWorldMap {
                let configuration = ARWorldTrackingConfiguration()
                configuration.initialWorldMap = worldMap
                configuration.planeDetection = .horizontal
                self.lblMessage.text = "Previous world map loaded"
                sceneView.session.run(configuration)
            }
        } else {
            let configuration = ARWorldTrackingConfiguration()
            configuration.planeDetection = .horizontal
            sceneView.session.run(configuration)
        }
    }
    @IBAction func saveButton(_ sender: UIButton) {
        saveMap()
    }
    func clearMap() {
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = .horizontal
        self.lblMessage.text = "Tap to place a virtual object"
        sceneView.debugOptions = [.showWorldOrigin, .showFeaturePoints]
        let options: ARSession.RunOptions = [.resetTracking, .removeExistingAnchors]
        sceneView.session.run(configuration, options: options)
    }
    @IBAction func clearButton(_ sender: UIButton) {
        clearMap()
    }
    @IBAction func loadButton(_ sender: UIButton) {
        loadMap()
    }
}
To test this app, follow these steps:
  1. 1.

    Connect an iOS device to your Macintosh through its USB cable.

     
  2. 2.

    Click the Run button or choose Product ➤ Run.

     
  3. 3.

    Aim your iOS device’s camera at a flat surface with plenty of distinctive features and tap the screen to place blue boxes in the augmented reality view, as shown in Figure 17-2.

     
../images/469983_1_En_17_Chapter/469983_1_En_17_Fig2_HTML.jpg
Figure 17-2

Placing virtual objects on a distinctive flat surface

  1. 4.

    Press the Home button twice on your iOS device to display a list of currently running apps (or swipe up on the Home screen and pause on an iOS device without a Home button). All currently running apps appear as thumbnail images, as shown in Figure 17-3.

     
../images/469983_1_En_17_Chapter/469983_1_En_17_Fig3_HTML.jpg
Figure 17-3

Displaying currently running apps as thumbnail images

  1. 5.

    Swipe up on the Persistence app thumbnail (or firmly press on the Persistence app thumbnail and tap the minus sign inside a red circle on an iOS device without a Home button).

     
  2. 6.

    Return to the Home screen and tap on the Persistence icon to load the app again, as shown in Figure 17-4.

     
../images/469983_1_En_17_Chapter/469983_1_En_17_Fig4_HTML.jpg
Figure 17-4

Finding the Persistence icon on the iOS device screen again

  1. 7.

    Tap the Load button on the Persistence app. The message Previous world map loaded appears.

     
  2. 8.

    Aim your iOS device’s camera at the flat surface where you had previously placed one or more blue boxes in the augmented reality view. As soon as the app recognizes the same area, it displays the virtual objects you placed earlier. (You must place your virtual objects on a flat surface with lots of distinctive features to make it easy for ARKit to recognize the same area and display the saved virtual objects in the same location again. If ARKit cannot recognize the area, it won’t be able to display the saved world map.)

     
  3. 9.

    Tap the Clear button to remove all virtual objects from the augmented reality view. At this point, you can repeat Steps 3–8 again.

     
  4. 10.

    Click the Stop button or choose Product ➤ Stop.

     

Summary

Persistence gives your app a way to save the current augmented reality view so the user can load it and view it again. This can be handy if an augmented reality session can take place over an extended period of time, such as an augmented reality game where users can save the game and return to it at another time.

Saving a world map in the user defaults is the simplest way to store an augmented reality view. The key is simply giving the user the ability to save a world map, load it at a later time, and clear the augmented reality view.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.174.239