Swiftで開発を行なっているiOSアプリにUnityを組み込む（Unity as a Library）

# TL;DR
Swiftで新規開発を行なっているiOSアプリに、Unityを組み込んだ経験をまとめます。
公式のサンプルではObjective-Cでの実装のみである中で、Swiftでの実装事例の１つとなれば幸いです。

また自分の探し方の問題かもしれませんが、Unity as a Library (UaaL)をSwiftのiOSアプリに組み込む英語の資料があまり見つかりませんでした。何かおすすめがありましたら教えていただけると幸いです！

# ポイント
1. [Unityの起動時にローディングの時間がかかるので、アプリを立ち上げた時点でUnityを起動しバックグラウンドでローディング](#アプリ起動時にunityを呼びバックグラウンドでローディング)
2. [アプリの画面遷移をNavigationControllerで実現したかったため、UnityのViewControllerからViewのみを呼び出す](#unityのviewcontrollerからviewのみを呼び出す)
3. [今回のアプリでは実装のメインがSwiftで、背景としてUnityを使うという方針にしたため、iOSアプリ→Unityの呼び出しのみ実装](#iosアプリunityの呼び出し-calling-c-back-from-native-code)


# 参考にした記事
**[[Swift] Unity as a LibraryをSwiftから呼ぶ](https://qiita.com/noppefoxwolf/items/b43d8554142e69c2ada6)**
主にこちらの記事を参考にしながら、iOSアプリのViewControllerからUnityのViewControllerのViewを呼び出す実装を進めました。


**[Unity as a LibraryのサンプルプロジェクトをSwiftで書き直した](https://note.com/hrkd/n/n2bb5fbbd407b)**
UaaLのサンプルをSwiftで書き換えてくださっているので、全体像を把握するために大いに活用させていただきました。
（今回、Unityの読み込み方法などがこちらとは異なります）

その他の便利な参考は[こちら](#その他の参考)

# 実装
## 前提
アプリのメインはSwiftで実装を行い、背景画像としてUnityの3Dモデルを読み込むようなアプリです。
また、メインのSwiftからUnityの関数を呼び出すことも行いました。

[**iOSアプリ設計パターン入門**](#その他の参考)
開発に当たっては、こちらの書籍を参考にMVPのアーキテクチャパターンを選択しました。


## アプリ起動時にUnityを呼び、バックグラウンドでローディング
### Unity Classの作成
様々な場所から呼び出せるよう、Unityをシングルトンオブジェクトとして実装します

```swift:Unity.swift
import Foundation

class Unity: NSObject, UnityFrameworkListener {
    static let shared = Unity()
    private let unityFramework: UnityFramework

    override init() {
        let bundlePath = Bundle.main.bundlePath
        let frameworkPath = bundlePath + "/Frameworks/UnityFramework.framework"
        let bundle = Bundle(path: frameworkPath)!
        if !bundle.isLoaded {
            bundle.load()
        }
        // It needs disable swiftlint rule due to needs for unwrapping before calling super.init()
        // swiftlint:disable:next force_cast
        let frameworkClass = bundle.principalClass as! UnityFramework.Type
        let framework = frameworkClass.getInstance()!
        if framework.appController() == nil {
            var header = _mh_execute_header
            framework.setExecuteHeader(&header)
        }
        unityFramework = framework
        super.init()
    }

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) {
        unityFramework.register(self)
        unityFramework.setDataBundleId("com.unity3d.framework")
        unityFramework.runEmbedded(withArgc: CommandLine.argc,
                                   argv: CommandLine.unsafeArgv, appLaunchOpts: launchOptions)
    }

    var view: UIView {
        unityFramework.appController()!.rootView!
    }

    func sendMessageToUnity(objectName: String, functionName: String, argument: String) {
        unityFramework.sendMessageToGO(withName: objectName, functionName: functionName, message: argument)
    }

    func applicationWillResignActive(_ application: UIApplication) {
        unityFramework.appController()?.applicationWillResignActive(application)
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        unityFramework.appController()?.applicationDidEnterBackground(application)
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        unityFramework.appController()?.applicationWillEnterForeground(application)
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        unityFramework.appController()?.applicationDidBecomeActive(application)
    }

    func applicationWillTerminate(_ application: UIApplication) {
        unityFramework.appController()?.applicationWillTerminate(application)
    }
}
```

### AppDelegateから呼び出す
アプリが立ち上がったタイミングでUnityを呼び出します。こうすることで、バックグラウンドでローディングされるため、スプラッシュ画面がユーザーに表示されず、待ち時間も発生しません。
新規のアプリなので、パフォーマンスなどにも今のところは影響ありません。

```swift:AppDelegate.swift
import Firebase
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        FirebaseApp.configure()

        // Unityを呼び出す
        Unity.shared.application(application, didFinishLaunchingWithOptions: launchOptions)

　　　　　// Unityとは関係のない、sign inの画面
        let singInViewController = SignInViewController(nibName: nil, bundle: nil)
        let navigationController = UINavigationController(rootViewController: singInViewController)

        let model = SignInModel()
        let presenter = SignInPresenter(view: singInViewController, model: model)
        singInViewController.inject(presenter: presenter)

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = navigationController
        window?.makeKeyAndVisible()
        return true
    }
}

```



## UnityのViewControllerからViewのみを呼び出す
[Integrating Unity into native iOS applications](#その他の参考)
Unity as a Libraryの仕組みとしては、メイン（ホスト）のiOSアプリのUIWIndowとは別に、Unity側でUIWindowを生成しています。そしてホスト側からUnity側のWindowに切り替える際には、`showUnityWindow`というUnityFrameworkの関数を呼び出す必要があります。（UIApplecation内に、複数のUIWindowが存在していて、最前面のUIWindowがアプリの画面として表示されているということになります。）
つまりUnityの画面をiOSアプリ内で表示する公式な方法としては、UnityのUIWindowを最前面に出すということになります。


一方で今回は、アプリの画面遷移をNavigationControllerで実現する必要がありました。
その実現に当たって、以下の２つの方法を検討しました。
**a. Unity側のWindowからViewだけを呼び出し、アプリの画面を表示しているViewControllerにaddSubViewする**
**b. Unity側のWindowを後ろに置いたまま、アプリの画面を表示しているViewControllerの背景を透明にすることで、Unity側のWindowを背景にする**

結論としては、a.を選択しました。やってみればすぐに分かることですが、b.のやり方だと、NavigatioControllerのアニメーションに対して、背景のUnity側Windowは動きません。つまり、NavigationControllerのアニメーションと連動せず違和感が発生することになるためです。

### Unity ClassのViewにアクセスできるようプロパティを実装
先ほどの、[Unity.swift](#unity-classの作成)から抜粋。

```swift:Unity.swift
...
 　　var view: UIView {
        unityFramework.appController()!.rootView!
    }
...
```

### ホスト側ViewControllerへのaddSubViewと、そのsubViewを背面へ移動
```swift:HostViewController.swift
import UIKit

class HostViewController: UIViewController {
    // UnityのViewの読み込み
    private let unityView = Unity.shared.view

    private var presenter: HostPresenterInput!
    func inject(presenter: HostPresenterInput) {
        self.presenter = presenter
    }

    init() {
        super.init(nibName: nil, bundle: nil)
        // addSubView
        view.addSubview(unityView)
        // 追加したsubViewのサイズをViewControllerのViewのサイズに合わせる
        unityView.frame = view.frame
        // 追加したsubViewを背面へ（addSubViewは最前面に追加するため、ViewControllerのViewの後ろに設定する必要がある）
        view.sendSubviewToBack(unityView)
    }
    
    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.isNavigationBarHidden = true
    }
}
```


## iOSアプリ→Unityの呼び出し （Calling C# back from native code）
iOSアプリからUnityに対して、データの受け渡しや関数の呼び出しを行います。
メインの開発はSwiftを用いたiOSアプリの開発で、Unityは背景として読み込むという方針を取りました。そのため、Unity→iOSのやりとりは発生しません。
今回は、簡易な`UnitySendMessage`を選択しました。
[Building plug-ins for iOS](https://docs.unity3d.com/Manual/PluginsForIOS.htmlhttps://docs.unity3d.com/Manual/PluginsForIOS.html) （Calling C# back from native code）

**補足**
今回Unity→iOSアプリの呼び出しが存在しなかったため、次の手順にある`NativeCallProxy.h`のヘッダーファイルは存在しません。
[Integrating Unity as a library into standard iOS app](https://github.com/hrkd/uaal-example/blob/master/docs/ios.md) （5. Expose NativeCallProxy.h）

### UnityFrameworkのメソッドを呼び出す
今回MVPのアーキテクチャパターンを選択したため、Modelからの呼び出しになっています。

```swift:HostModel.swift
import Foundation

protocol HostModelInput {
    func sendData(data: [String?])
}

final class HostModel: HostModelInput {
    func sendData(data: [String?]) {
        // 引数はStringなので、それに合わせる
        let dataText =
            """
            {\"data0\": \(data[0]!), \"data1\": \(data[1]!), \"data2\": \(data[2]!),
            \"data3\": \(data[3]!), \"data4\": \(data[4]!), \"data5\": \(data[5]!)}
            """
        // UnityFrameworkdのメソッドを呼び出す
        Unity.shared
            .sendMessageToUnity(objectName: "SampleData", functionName: "SetData", argument: dataText)
    }
}
```

# その他の参考
[**iOSアプリ設計パターン入門**](https://peaks.cc/ECFAE051/iOS_architecture)
開発に当たっては、こちらの書籍を参考にMVPのアーキテクチャパターンを選択しました。（選択の過程をまとめた記事はこちら→[PoCでiOSアーキテクチャにMVPを採用した話](https://qiita.com/pkino/items/403a35156a80b5ffb416)）
iOSアプリ開発に限らず、設計パターンを学びたい方に損得抜きで**強くおすすめ**です。歴史的経緯も含めた設計パターンの丁寧な解説、さらに各設計パターンのサンプルコードで構成されています。

[**【Unity】MirrativのEmbedding Unityを更新した話: 実践 Unity as a Library**](https://tech.mirrativ.stream/entry/2020/10/20/100000)
[【Unity】Mirrativのアバターがなんで動いているのか誰にもわからないので説明する](https://tech.mirrativ.stream/entry/2019/12/04/100000)
実際にサービスを運用されている事例から、実践的な説明がされています。
2本目はUnity as a Library (UaaL)が実装される前に、iOSアプリにembedを行なった方法を説明されています。
**（Unityに詳しい先輩曰く、「黒魔術のようなものなので、今はUaaLを使うべき」とのことでした！）**


[**Integrating Unity into native iOS applications**](https://docs.unity3d.com/Manual/UnityasaLibrary-iOS.html)
公式のドキュメントで、iOSアプリから呼び出せるメソッドが説明されています。


# こぼれ話
[Using Unity as a Library in other applications](https://docs.unity3d.com/2019.3/Documentation/Manual/UnityasaLibrary.html?_ga=2.43810085.1054171678.1617266996-1207030884.1617266996)
Unity as a Library is intended for specialist users who use native platform technologies such as Java/Android, Objective C/iOS, or Windows Win32/UWP, and want to include Unity-powered features in their games or applications.
> Unity as a Libraryは、Java/Android、Objective C/iOS、Windows Win32/UWPなどのネイティブプラットフォーム技術を使用し、ゲームやアプリケーションにUnity搭載の機能を搭載したいと考えている**専門家ユーザー**を対象としています。

公式の紹介ページには、上記の脅し文句があります。iOSアプリ開発自体が初めての自分は最初少しひるみましたが、これ以外に方法がないので意を決して取り組みました。確かにUnityがiOSアプリの中でどのように読み込まれるのか、ライフサイクルにどのように組み込まれるべきなのかなど理解に時間がかかった部分もあります。しかしながら、この記事内で紹介している記事を一通り読んでいただければ、全体像は掴めるのではないかと思います。Unityをネイティブアプリに組み込みたくなった方は、不可能なことではないのでぜひ挑戦してみてください！また、不明点などがあれば一緒に学ばせていただければと思いますので、コメントください。