SwiftUIでARKitを触って考えたこと



#はじめに

SwiftUIが2019年に登場してそろそろ2年になろうとしているところ、仕様も確立しつつあり、SwiftUIに移行し始めている方も多いかと思います。

しかしARKitを使ったアプリとなると話は別で、調べてもあまり情報が出てきません。Apple公式から出ているARアプリのSampleもまだStoryboardによる実装です。
例えばLiDARを使ったPointCloudとか。
https://developer.apple.com/documentation/arkit/visualizing_a_point_cloud_using_scene_depth 

確かにARアプリとなるとPinchやRotateなどUIGestureRecognizerによる操作が中心になるので、既存のUIViewControllerとセレクタによる実装の方が直感的にとっつきやすいです。

そういう事情はあれ、これからはSwiftUIによるアプリ開発が主流になると思うので、この記事ではSwiftUIでARKitを使った際のポイントについて書いていきます。

#環境

- Swift 5.3
- Xcode 12.2
- ARKit 4

#UIView/UIViewControllerの実装

まずSwiftUIのプロジェクトからAugmented Reality Appを選択すると以下のように初期コードが実装されます。

```swift
import SwiftUI
import RealityKit

struct ContentView : View {
    var body: some View {
        return ARViewContainer().edgesIgnoringSafeArea(.all)
    }
}

struct ARViewContainer: UIViewRepresentable {
    
    func makeUIView(context: Context) -> ARView {
        let arView = ARView(frame: .zero)
        let boxAnchor = try! Experience.loadBox()
        arView.scene.anchors.append(boxAnchor)
        return arView
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {}
}

//Preview部分は省略
```

SwiftUIでUIKitを使う場合はRepresentableによる実装となります。
このコードではARViewContainerの部分ですね。
このARViewContainerを上のContentViewで読み込むことにより、画面に表示する形となります。

AR空間を表示するARViewはARViewContainerの``makeUIView(context:)``で実装しています。
下の``updateUIView(_:context:)``でARViewの更新を管理することになります。

一方でUIViewContorollerを使う場合はこちら。

```swift
import SwiftUI
import RealityKit

struct ContentView : View {
    var body: some View {
        return ARViewControllerContainer().edgesIgnoringSafeArea(.all)
    }
}

struct ARViewControllerContainer: UIViewControllerRepresentable {
    
    typealias UIViewControllerType = ARViewController
    
    func makeUIViewController(context: Context) -> ARViewController {
        return ARViewController()
    }
    
    func updateUIViewController(_ arViewController: ARViewController, context: Context) {}
}

class ARViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let arView = ARView(frame: .zero)
        
        let boxAnchor = try! Experience.loadBox()
        arView.scene.anchors.append(boxAnchor)
        
        self.view = arView
    }
}

//Preview部分は省略
```

UIViewRepresentableと比べてARViewControllerのクラスが増えました。
このARViewControllerは必須というわけではなく、例えば``viewDidLoad()``の中身をそのままARViewControllerContainerに移しても使えます。

ですからARViewのみを使うのであればUIViewRepresentableでの実装で問題ないと思います。

ちなみに上の2つのサンプルを実装するとこうなります。
![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/620e1d77-23b9-0843-4548-8112de21955c.gif)

#UIGestureRecognizerの実装

UIViewcontrollerRepresentableを使うとこれまでのUIGestureがそのまま使えます。

```swift
class ARViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let arView = ARView(frame: .zero)
        
        let boxAnchor = try! Experience.loadBox()
        arView.scene.anchors.append(boxAnchor)
        
        self.view = arView
        
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tap))
        self.view.addGestureRecognizer(tapGesture)
    }
    
    @objc
    func tap(sender: UITapGestureRecognizer) {}
}
```

ただ、SwiftUIのような新しいフレームワークを出したということは、これまで以上にObjective-Cから脱却していくような感じなりそうな中で、ずっと``@objc``を使うのが良いのかどうかは微妙なところです。

しかしながらSwiftUIでGestureを実装しようとすると、最初に``onTapGesgure{}``などで値を取得して、それを``@State``や``@Binding``でRepresentableに送って、そこからSCNNodeやModelEntityなどのプロパティを変更して・・・と手順を踏む必要があります。

恐らくAppleとしてはTapのような基本的な動作はRealityComposerで設定して、RotateやPanのような動作はコードで実装して欲しいというところではないかと思いますが、現状ではこれまで通りの実装でも問題無さそうではあります。

#感想

個人的には最初に書いたようにUIViewControllerとセレクタによる実装はとっつきやすいです。
ただ、今後この方法が使えなくなる可能性もありますし、SwiftUIを使ったもっと簡単な仕組みを実装しそうでもあります。

あとはSwiftUIだとコードを書いたらそのまま複数デバイスで使用することができるので、Storyboardのようにデバイスごとに細かくボタンなどの位置を調整して・・・という作業がなくなるのが非常に助かります。
これはARアプリ以外にも当てはまりますね。

これからAppleはSwiftUIを中心にコンテンツを追加していきますし、ARKitもどんどん発展・改善されていきそうなので、早いところSwiftUIによる実装に慣れておいた方がいいのかなと思いました。