ARKit2.0でPresistent ARを実装してみる

[本家](https://developer.apple.com/documentation/arkit/arworldmap)のサンプルコードがMultiuser ARしか無いのでPresistent ARを試してみた。

# 準備

## iOS12、Xcode12
ARKit2.0はiOS12以降が対応しているので、iOS12の開発環境が必要になる。
2018年7月現在、iOS12とXcode12はbeta版の公開のみなので、自己責任でインストールする必要がある。

言語はswift4（ちなみに初心者）。

# Augmented Reality Appから始める
Application > Augmented Reality Appを選択。

# タップした位置に箱を表示する

viewDidLoadを修正。

```swift
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set the view's delegate
        sceneView.delegate = self
        // 特徴点の表示
        sceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints
        
        // Create a new scene
        let scene = SCNScene()
        
        // Set the scene to the view
        sceneView.scene = scene
        
        // タップジェスチャーの登録
        let gesture = UITapGestureRecognizer(target: self, action: #selector(tapView))
        sceneView.addGestureRecognizer(gesture)
    }
```

tapViewの実装。タップした位置にアンカーを追加。

```swift
    @objc func tapView(sender: UITapGestureRecognizer) {
        guard let hitTestResult = sceneView
            .hitTest(sender.location(in: sceneView), types: [.existingPlaneUsingGeometry, .estimatedHorizontalPlane])
            .first
            else { return }
        let anchor = ARAnchor(name: "box", transform: hitTestResult.worldTransform)
        sceneView.session.add(anchor: anchor)
    }
```

sessionにAnchorを追加した時の動作を記述。タップ時や読出時に、箱が追加される。

```swift
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        if let name = anchor.name, name.hasPrefix("box") {
            let geometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
            let material = SCNMaterial()
            material.diffuse.contents = UIColor.darkGray
            geometry.materials = [material]
            
            let addnode = SCNNode(geometry: geometry)

            node.addChildNode(addnode)
        }
    }
```

ここまでで、タップした位置に箱が表示されるアプリができている。

# ARWorldMapの保持、初期化、読出をする

ローカルに保存する保存先として`saveURL`を定義しておく。

```swift
...
class ViewController: UIViewController, ARSCNViewDelegate {
    // 拡張子は適当
    let saveURL = "arworldmap.dat"
...
```

Main.storyboardでARSCNView上に「SAVE」「INIT」「LOAD」ボタンを配置し、touchDownイベントをViewControllerの`@IBAction`関数とする。

```swift
    @IBAction func saveTouchDown(_ sender: Any) {
    }

    @IBAction func initTouchDown(_ sender: Any) {
    }

    @IBAction func loadTouchDown(_ sender: Any) {
    }
```

それぞれボタン押下時の動作を実装していく。

```swift
    @IBAction func saveTouchDown(_ sender: Any) {
        sceneView.session.getCurrentWorldMap { worldMap, error in
            guard let map = worldMap
                else { print("Error: \(error!.localizedDescription)"); return }
            guard let data = try? NSKeyedArchiver.archivedData(withRootObject: map, requiringSecureCoding: true)
                else { fatalError("can't encode map") }

            if let dir = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask ).first {
                
                let path_file_name = dir.appendingPathComponent( self.saveURL )
                
                guard ((try? data.write(to: path_file_name)) != nil) else { return }
            }
            
            self.displayAlert(message: "保存しました")
        }
    }

    @IBAction func initTouchDown(_ sender: Any) {
        sceneView.scene.rootNode.enumerateChildNodes { (node, stop) in
            node.removeFromParentNode()
        }

        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = .horizontal
        sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])

        self.displayAlert(message: "初期化しました")
    }

    @IBAction func loadTouchDown(_ sender: Any) {
        var data: Data? = nil
        if let dir = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask ).first {
            
            let path_file_name = dir.appendingPathComponent( self.saveURL )
            
            do {
                try data = Data(contentsOf: path_file_name)
            } catch {
                return
            }
        }

        guard let worldMap = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self, from: data!) else { return }
        
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = .horizontal
        configuration.initialWorldMap = worldMap
        sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])

        self.displayAlert(message: "読み込みました")
    }
```

メッセージをアラート表示する`displayAlert`の実装。

```swift
    func displayAlert(message: String){
        let alert = UIAlertController(title: "アラート", message: message, preferredStyle: UIAlertController.Style.alert)
        let okayButton = UIAlertAction(title: "ok", style: UIAlertAction.Style.cancel, handler: nil)
        alert.addAction(okayButton)
        
        self.present(alert, animated: true, completion: nil)
    }
```

これで
1. タップした位置に箱を表示する
2. 「SAVE」をタップしてARWorldMapを保存する
3. 「INIT」をタップして初期化する（わかりやすくするため）
4. 「LOAD」をタップしてARWorldMapを読出して再スタート
5. １.と同じ場所を映すと同じ位置に箱が表示される
ができる。