Unity as a LibraryをXCFramework化してiOSの実機とシミュレーターの両方で動かせるようにする

# 更新履歴

**2022/06/12 更新**

- **Apple Silicon搭載Macでの動作確認結果について追記**
- UnityとXcodeのバージョンを更新
    - Unity 2021.3.4f1
    - Xcode 13.4.1

---

**[Unity as a Library](https://unity.com/ja/features/unity-as-a-library)**(以降、`UaaL`と省略)をiOSネイティブアプリに組み込んだ上でシミュレーターで動かそうとした所、以下のような課題に直面しました。

- **Unityが実機向け or シミュレーター向けのどちらかでしかビルドできない問題**
    - UnityのiOSビルドは元より「実機向け」か「シミュレーター向け」かのどちらかでしかビルド出来ないという制約が存在するが、**UaaLを組み込んだネイティブアプリ側でも同じ制約が発生してしまう**
        - → 例えば「[Player Settings -> Target SDK](https://docs.unity3d.com/Manual/class-PlayerSettingsiOS.html#Config-Device)」に`DeviceSDK`を指定してビルドしたUaaLをネイティブアプリに組み込んだ場合には、<font color="Red">ネイティブアプリ側も実機でしかビルドできなくなる</font>
- **それ以前に「Target SDK」に`SimulatorSDK`を指定してビルドしたUaaLをネイティブアプリに組み込んだ場合には、<font color="Red">ビルド時にエラーが発生する</font>**


今回はその後者で挙げている「**ビルドエラーの回避方法**」及び、前者で挙げている課題に対するアプローチとしてタイトルにもある「**UaaLを`XCFramework`化することで実機とシミュレーターの両方で動かせるようにする手順**」について解説していければと思います。

もし間違いや冗長な箇所などあればコメントにて教えて頂けると幸いです。 

:::note warn
詳細については各章にて記載しますが、今回提案する方法は一部理解しきれていない状態で適用しているところがあるので、導入については自己責任でお願いします。。 :bow: 
:::

**▼ バージョン**

- Unity 2021.3.4f1
- Xcode 13.4.1

**▼ 環境**

- MacBook Pro (16-inch, 2019)
    - **2.4 GHz 8コアIntel Core i9**
- Mac Studio (2022)
    - **M1 Max**

:::note warn
今回紹介する手法は**Apple Siliconを搭載したMacでも適用可能**ですが...<font color="Red"><b>シミュレーターで実行する際には「XcodeをRosetta経由で実行しておく」必要があります。</b></font>
※軽く検証した話を[こちらで追記](#apple-silicon対応について)
:::


**▼ 想定読者**

こちらはUaaLのiOSに特化した記事となるので、想定読者としては「**UnityとiOSネイティブアプリ両方の開発知識を多少なりとも持っている方**」を対象に書いてます。

ここらについては深くまで解説しない点もあるのでご了承下さい。
(例えばプロジェクトの作成やビルド手順と言った基礎的なところなど)


**▼ 省略表記について**

冒頭で書いた「Unity as a Library → `UaaL`」と言う省略の他にも、記事中では以下のような省略(言い回し)で解説していきます。

- [Player Settings -> Target SDK](https://docs.unity3d.com/Manual/class-PlayerSettingsiOS.html#Config-Device)に「[**DeviceSDK**](https://docs.unity3d.com/ScriptReference/iOSSdkVersion.DeviceSDK.html)」を指定したビルド　→ 実機向けのビルド
- [Player Settings -> Target SDK](https://docs.unity3d.com/Manual/class-PlayerSettingsiOS.html#Config-Device)に「[**SimulatorSDK**](https://docs.unity3d.com/ScriptReference/iOSSdkVersion.SimulatorSDK.html)」を指定したビルド　→ シミュレーター向けのビルド



# サンプルプロジェクト

今回動かすサンプルはこちら。

ネイティブアプリ側はUIKit+SwiftUIを併用して実装しており、UaaLはSwiftUIのパーツの一部として組み込むような構成となってます。
その上でUnity上とネイティブアプリ上にある両方のスライダーからはUnity上に表示されているキューブの回転速度を双方向から設定できるようにしてます。

![20220604_205554.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/6990f997-a111-1ff7-7475-af4812049c32.gif)

:::note info
ネイティブプラグインの導入の有無でビルド結果が変わってエラーが出たりすると怖い[^3]ので、今回はプラグインを実装している前提で「Unity⇔ネイティブ」の双方向でデータのやり取りを出来るところまで用意してます。
([ちなみにネイティブプラグインはSwiftオンリーで実装](https://qiita.com/mao_/items/07466e169d08cbeff221))
:::

[^3]: 例えば「プラグインを実装する前ならエラーは出なかったけど、プラグインを実装したらエラーが出るようになった」的な現象も無きにしもあらずなので...

プロジェクトはGitHubにて公開しているので、詳細はこちらを御覧ください。
その上で各章ごとにブランチを切っているので順に解説していきます。

- [mao-test-h/UaaL-Examples-iOS](https://github.com/mao-test-h/UaaL-Examples-iOS)





# シミュレーター向けにビルドしたUaaLをネイティブアプリに組み込むとビルドエラーが発生する問題を解消する

`XCFramework`化の話に入る前に先ずはこちらの章から解説します。
**(これを解消しない分にはシミュレーターのビルドは通らないままなので...)**

--

この章ではUnity側でシミュレーター向けにビルドしたものを[UaaL公式サンプルと同じ手順](https://github.com/Unity-Technologies/uaal-example/blob/master/docs/ios.md)でネイティブアプリに組み込み、その際に**ネイティブアプリ側でシミュレーター向けにビルドしようとした際に恐らくは発生するであろうビルドエラーの回避方法について解説していければと思います。** 

:::note
後述する`XCFramework`化の話と混ざらないように、この章では敢えて[UaaL公式サンプルと同じ手順](https://github.com/Unity-Technologies/uaal-example/blob/master/docs/ios.md)をベースに解説していきます。
(その為、UaaLを組み込んだネイティブアプリ側では実機とシミュレーターの両方で実行することは不可のままなので注意)
:::

完成品のサンプルは **[feature/xcworkspace](https://github.com/mao-test-h/UaaL-Examples-iOS/tree/feature/xcworkspace)** ブランチを御覧ください。
(動作確認方法はREADMEを参照)



## 発生するビルドエラーについて

具体的な解説に入る前に、**「同じエラーが発生しているのか？」の認識合わせとして現在私の手元で発生しているビルドエラーについても触れておきます。**

エラーの内容としては、ネイティブアプリ本体(サンプルだと`UaaLExample`と言うターゲット)のビルドフェーズにて**以下のようなリンクエラーが発生しており、** エラーログを引用するとこのように出力されてます。

```shell
# Build target UaaLExample
# Project UaaLExample| Configuration Debug | Destination iPhone 13 Pro Max | SDK Simulator - iOS 15.5

Showing Recent Messages
Ld (長いので省略)

Undefined symbols for architecture x86_64:
  "protocol descriptor for UnityFramework.NativeCallsProtocol", referenced from:
      protocol conformance descriptor for UaaLExample.Unity : UnityFramework.NativeCallsProtocol in UaaLExample in Unity.o
  "type metadata accessor for UnityFramework.FrameworkLibAPI", referenced from:
      UaaLExample.Unity.application(_: __C.UIApplication, didFinishLaunchingWithOptions: [__C.UIApplicationLaunchOptionsKey : Any]?, onReadyHandler: () -> ()) -> () in Unity.o
  "_OBJC_CLASS_$_UnityFramework", referenced from:
      objc-class-ref in Unity.o
  "static UnityFramework.FrameworkLibAPI.registerAPIforNativeCalls(UnityFramework.NativeCallsProtocol) -> ()", referenced from:
      UaaLExample.Unity.application(_: __C.UIApplication, didFinishLaunchingWithOptions: [__C.UIApplicationLaunchOptionsKey : Any]?, onReadyHandler: () -> ()) -> () in Unity.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
```

ここで表示されるエラーログの内容自体は各々の環境やプロジェクトの内容(更に言うと実装しているネイティブプラグインのクラス名など)によって変わってくるかと思われますが、もし近い内容のエラーが出ているとしたら、**恐らくは次で話す回避方法を適用できる可能性があります。**

逆にこれとは全く違う系統のエラーが発生している場合には別要因である可能性があるために、今回の回避方法は適用できないかもしれないのでご了承下さい。



## 回避方法について

先に結論から言うと、Xcode上から`Unity-iPhone -> UnityFramework`の「Build Settings」を開き、**「Linking -> Other Linker Flags」から以下の2つを取り除くことでビルドが通るようになります。**

> -Wl,-undefined,dynamic_lookup
> -Wl,-exported_symbol,_il2cpp_*

![スクリーンショット 2022-05-28 17.34.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/02e8adf6-8fbf-ba2e-dff5-e071a34006cc.png)

![スクリーンショット 2022-05-28 17.34.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/1cda7c1d-e3e3-f927-3e4d-1fb75ef9679a.png)



## エラーの原因について

エラーの原因についても軽く考察していきます。

「実機向けにビルドしたUaaL」と「シミュレーター向けにビルドしたUaaL」を用意して、それぞれをネイティブアプリに組み込んだ状態でビルド時のログを見比べてみた所、**後者のシミュレーター向けの方では`UnityFramework`のビルドフェーズにて以下の警告が出力されているのを確認しました。**

```shell
# Build target UnityFramework
# Project Unity-iPhone | Configuration Debug | Destination iPhone 13 Pro Max | SDK Simulator - iOS 15.5

Showing Recent Messages
Ld (長いので中略) -Wl,-undefined,dynamic_lookup -Wl,-exported_symbol,_il2cpp_\* (長いので中略)

ld: warning: -undefined dynamic_lookup is deprecated on iOS Simulator
ld: warning: cannot export hidden symbol _il2cpp_no_exceptions from ~/Library/Developer/Xcode/DerivedData/(プロジェクトによって変わるので省略)/Build/Intermediates.noindex/Unity-iPhone.build/Debug-iphonesimulator/UnityFramework.build/Objects-normal/x86_64/Il2CppOptions.o
ld: warning: cannot export hidden symbol _il2cpp_runtime_stats from ~/(プロジェクトのあるディレクトリ)/UaaL-Examples-iOS/UnityProject/Builds/UnityFramework/Libraries/libil2cpp.a(External_il2cpp_builds_libil2cpp_0_8tnvs.o)
ld: warning: cannot export hidden symbol _il2cpp_defaults from ~/(プロジェクトのあるディレクトリ)/UaaL-Examples-iOS/UnityProject/Builds/UnityFramework/Libraries/libil2cpp.a(External_il2cpp_builds_libil2cpp_vm_5_8tnvs.o)
```

上記の内容からldコマンドを叩いている所(リンク実行時)で怒られているのが見えてきたので、更に両者のリンク時の設定(Xcode上の「Build Settings -> Linking」)を見比べてみた所、**シミュレーター向けにビルドした場合には「Other Linker Flags」に先程挙げた以下2つのパラメータが渡されているのを確認しました。**
(逆に実機向けにビルドした場合には以下2つのパラメータは渡されていない)

> -Wl,-undefined,dynamic_lookup
> -Wl,-exported_symbol,_il2cpp_*

正直な所、私自身がldコマンド周りについてあまり詳しくないので、これらがどの様に作用しているのかまでについては説明できるレベルまで理解しきれていませんが...今回の警告内容を見るに恐らくはこれらが影響しているだろうと仮定して、試しに両者をパラメータから削除してみた所、ビルドが通るようになったという経緯があります。


:::note warn
記載の通り詳細まで理解しきれていない状態で導入している回避方法にはなりますが...シミュレーター向けの時のみ発生しているエラーと言うのもあるので、私個人としては「一旦は適用しつつも様子見で良いかな...」という感じで取り入れてます。。
→ 今後、仮に上記の設定変更が影響して別要因のエラーが発生するなどと言った現象を確認した場合には随時追記していきます。

その上でもし今回のエラー原因について詳しい方が居ましたら、コメントなりで教えて頂けると幸いです。。 :bow: 
:::




## `[PostProcessBuild]`を用いて設定変更を自動化する

今回の回避方法の適用先はUnityがビルド時に出力するXcodeプロジェクトの範囲内となるために、**[PostProcessBuildAttribute](https://docs.unity3d.com/ScriptReference/Callbacks.PostProcessBuildAttribute.html)を用いたEditor拡張を実装することで設定変更を自動化することが可能です。**
(所謂、[こちらの記事](https://qiita.com/mao_/items/c678f93ee04608492788)の内容)

以下に今回サンプルプロジェクトに導入しているコードを載せます。

```csharp:XcodePostProcess.cs
static class XcodePostProcess
{
    // 参考: https://tech.mirrativ.stream/entry/2020/10/20/100000
    [PostProcessBuild]
    static void OnPostProcessBuild(BuildTarget target, string path)
    {
        if (target != BuildTarget.iOS) return;

        var projectPath = PBXProject.GetPBXProjectPath(path);
        var project = new PBXProject();
        project.ReadFromString(File.ReadAllText(projectPath));

        var targetGuid = project.GetUnityFrameworkTargetGuid();

        // `Data`フォルダを[Build Phase -> Copy Bundle Resources]に追加
        var dataPathGuid = project.FindFileGuidByProjectPath("Data");
        var resPhaseGuid = project.GetResourcesBuildPhaseByTarget(targetGuid);
        project.AddFileToBuildSection(targetGuid, resPhaseGuid, dataPathGuid);

        // Swift version
        project.SetBuildProperty(targetGuid, "SWIFT_VERSION", "5.0");

        // NOTE: iOS Simulatorのエラー対策
        // → この章で解説している内容
        if (PlayerSettings.iOS.sdkVersion == iOSSdkVersion.SimulatorSDK)
        {
            var removeLdFlags = new[]
            {
                "-Wl,-undefined,dynamic_lookup",
                "-Wl,-exported_symbol,_il2cpp_*"
            };
            project.UpdateBuildProperty(targetGuid, "OTHER_LDFLAGS", new string[] { }, removeLdFlags);
        }

        project.WriteToFile(projectPath);
    }
}
```

:::note
上記の`XcodePostProcess`では[この章で解説した回避方法](#回避方法について)の適用の他にも、以下の設定も自動で適用するようにしてます。

- Dataフォルダを`UnityFramework`に加える
    - → [UaaL公式サンプルの手順](https://github.com/Unity-Technologies/uaal-example/blob/master/docs/ios.md)で言う、「6. Make Data folder to be part of the UnityFramework」のステップに該当
- Swiftバージョンの指定 [^2]

他にも自動で適用したい設定がある場合には、こちらで変更を加えることが可能です。(e.g. bitcodeの生成など)

参考: [【Unity】MirrativのEmbedding Unityを更新した話: 実践 Unity as a Library](https://tech.mirrativ.stream/entry/2020/10/20/100000)
:::

[^2]: 最近のUnityだと(?)最初から「Swift 5.0」が指定されているっぽいので実際には無くても良いかもしれないが...一応明示的に指定するようにはしている。





# UaaLを`XCFramework`化することで実機とシミュレーターの両方で動かせるようにする

シミュレーターのビルドエラーの回避方法が分かったので、ここからはタイトルにもある「**UaaLを`XCFramework`化することで実機とシミュレーターの両方で動かせるようにする**」手順について解説していきます。

:::note warn
ちなみに[前の章で解説したエラー](#発生するビルドエラーについて)は[回避方法](#回避方法について)を適用しないと、この章で解説する`XCFramework`化を行っても発生するので注意。
:::

完成品のサンプルは **[feature/xcframework](https://github.com/mao-test-h/UaaL-Examples-iOS/tree/feature/xcframework)** ブランチを御覧ください。
(動作確認方法はREADMEを参照)


## その前に`XCFramework`とは何か？

`XCFramework`とはXcode11で対応されたバイナリフレームワークの形式であり、例えばこちらを用いることで「実機向け(arm64)」「シミュレーター向け(Intel Macだとx86_64)」と言ったアーキテクチャごとに異なる実態を持つフレームワークを1つに纏めることができ、**実際にそれを組み込むアプリ側からは実機やシミュレーター、各種プラットフォームなどを意識せずに「1つのフレームワーク」として透過的に取り扱うことが出来るようになります。**
(拡張子は`.xcframework`)

ここらについてはWWDCの講演及び以下の記事が参考になります。

- [Binary Frameworks in Swift - WWDC19](https://developer.apple.com/videos/play/wwdc2019/416/)
- [OpenCV を XCFramework にして Swift Package Manager 経由で iOS で使ってみた](https://tech-blog.optim.co.jp/entry/2020/12/17/100000)
- [xcframeworkを作成する（第1回）](https://qiita.com/tfactory/items/34f9d88f014c47221617)


### 【補足】 そもそもフレームワークとは何か？
 
一応こちらについても補足しておくと、フレームワーク(`.framework`)とはAppleプラットフォーム上で利用可能なライブラリの形式の一つであり、[公式ドキュメント](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WhatAreFrameworks.html#//apple_ref/doc/uid/20002303-BBCEIJFI)から一部和訳して引用すると以下のように記載されてます。

> 動的共有ライブラリ、nibファイル、イメージファイル、ローカライズ文字列、ヘッダーファイル、リファレンスドキュメントなどの共有リソースを単一のパッケージにカプセル化した階層的なディレクトリです。

詳細については公式ドキュメント及び以下の記事が参考になります。

- [What are Frameworks?](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WhatAreFrameworks.html#//apple_ref/doc/uid/20002303-BBCEIJFI)
- [Swift で Framework を作ってみる使ってみる](https://qiita.com/codelynx/items/1ff68b7db15bd9d233b7)
- [Static Library vs Dynamic Library](https://yamatooo.blog/entry/2020/10/03/083000)

ちなみにUaaLも実態としてはフレームワークであり、iOSビルドの出力結果(`Unity-iPhone.xcodeproj`)にある **`UnityFramework.framework`と言うターゲットがそれに該当します。** [^4]

[^4]: [UaaL公式サンプルにある手順](https://github.com/Unity-Technologies/uaal-example/blob/master/docs/ios.md)はその`UnityFramework.framework`を「UaaLとして公開するための設定(`NativeCallProxy.h`の公開や`Data`を含めるための設定)」及び「それをiOSネイティブアプリのプロジェクトに組み込むステップ」の一例について解説している


### 実機向けとシミュレーター向けの`UnityFramework.framework`を1つに纏めることが可能

話を戻すと、今回はその`XCFramework`を利用することで **「実機向けにビルドしたUaaL(`UnityFramework.framework`)」と「シミュレーター向けにビルドしたUaaL」を「1つの`XCFramework`」として纏めることが可能となります。**

その上でネイティブアプリ側での方でも統合された`XCFramework`を組み込むようにすれば、両方のアーキテクチャのバイナリが含まれているので**実機/シミュレーターを意識せずとも実行できるようになります。**

![スクリーンショット 2022-06-05 9.53.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/0556b9cd-803d-0e6b-8792-a599165778a5.png)


:::note warn
ちなみに、この手順については裏を返すと**Uinty側は実機とシミュレーターの両方をビルドする必要があったり**、他にも次で解説する`XCFramework`をビルドするまでに様々なステップが入ることになるので、**ビルド時間に関しては必然的に伸びることになります。**

なので、これから解説する手順を実際に導入するにあたっては、それぞれのマシンスペックやワークフローなどを踏まえた上で検討していくのが良いかと思います。
:::


## `XCFramework`のビルド手順

大まかな手順としては以下のようになります。

1. UnityのiOSビルドを実行して実機 & シミュレーター向けの2つの`Unity-iPhone.xcodeproj`を生成
2. ビルドした`.xcodeproj`から`.xcarchive`をビルド
3. `.xcarchive`から`.xcframework`をビルド
4. `[BUILD_LIBRARY_FOR_DISTRIBUTION]`を有効にしている都合で、このままだと`UnityFramework.xcframework`を組み込んだ際にエラーが発生するので対策
    - 参考: [Xcode 11 XCFramework "... is not a member type of ..." error](https://developer.apple.com/forums/thread/123253)

サンプルプロジェクトの方ではこれらの手順全てを以下のシェルスクリプトで実装してコマンドライン上から実行できるようにしているので、こちらから引用しつつ解説していきます。
(シェルスクリプト全体はこちら↓)

- [build_for_xcframework.command](https://github.com/mao-test-h/UaaL-Examples-iOS/blob/feature/xcframework/UnityProject/build_for_xcframework.command)

※余談: macOSだと拡張子を`.command`にすることでシェルスクリプトをダブルクリックで実行できるようになります。

### ▼ 1. UnityのiOSビルドを実行して実機 & シミュレーター向けの2つの`Unity-iPhone.xcodeproj`を生成

こちらは単純にUnity上でEditor拡張として実装しているビルドメソッドをコマンドライン上から呼び出しているだけです。
ここで呼び出されているコードは以下を御覧ください。

- [BuildMenu.cs](https://github.com/mao-test-h/UaaL-Examples-iOS/blob/feature/xcframework/UnityProject/Assets/UaaLExample/Scripts/Editor/BuildMenu.cs)

```shell:build_for_xcframework.command
# UnityEditorのパス
UNITY_EDITOR_PATH="/Applications/Unity/Hub/Editor/2021.3.4f1/Unity.app/Contents/MacOS/Unity"

# ビルド結果の出力先
OUTPUT_PATH="${CURRENT_DIR}/Builds"


# 1. UnityのiOSビルドを実行して実機&シミュレーター向けの2つの`Unity-iPhone.xcodeproj`を生成

# https://docs.unity3d.com/Manual/EditorCommandLineArguments.html
${UNITY_EDITOR_PATH} -batchmode \
    -nographics \
    -silent-crashes \
    -quit \
    -buildTarget iOS \
    -projectPath ${CURRENT_DIR} \
    -executeMethod UaaLExample.Editor.BuildMenu.BuildIOSForAllSDK

osascript -e 'display notification "Success Build UnityProject" sound name "Bip"'

# ビルドに成功して両方の`Unity-iPhone.xcodeproj`が生成されているかチェック
DEVICE_BUILD_PATH="${OUTPUT_PATH}/DeviceSDK/Unity-iPhone.xcodeproj"
SIMULATOR_BUILD_PATH="${OUTPUT_PATH}/SimulatorSDK/Unity-iPhone.xcodeproj"

if [ ! -d $DEVICE_BUILD_PATH ]; then
    echo "iOS実機向けのビルドが存在しない"
    return 1
fi

if [ ! -d $SIMULATOR_BUILD_PATH ]; then
    echo "iOSシミュレーター向けのビルドが存在しない"
    return 1
fi
```

完了したら`./UnityProject/Builds`以下に実機向けのビルドとシミュレーター向けのビルドの2つが生成されるので、問題なければ次の手順に進みます。



### ▼ 2. ビルドした`.xcodeproj`から`.xcarchive`をビルド

前の手順でビルドした成果物(実機&シミュレーター向けの`Unity-iPhone.xcodeproj`)から`xcarchive`をビルドします。
(実際に`.xcodeproj`に対して本格的なビルドが走るタイミングとなるので、ビルド時間の方は長くなる点に注意)

ここでビルドした`xcarchive`には`UnityFramework.framework`が含まれるので、それを次の手順で利用します。

```shell:build_for_xcframework.command
# ビルド結果の出力先
OUTPUT_PATH="${CURRENT_DIR}/Builds"


# 2. ビルドした`.xcodeproj`から`.xcarchive`をビルド

DEVICE_BUILD_ARCHIVE_PATH="${OUTPUT_PATH}/UnityFramework-Device.xcarchive"
SIMULATOR_BUILD_ARCHIVE_PATH="${OUTPUT_PATH}/UnityFramework-Simulator.xcarchive"

# 前回のビルド結果が残っている場合には先に削除
rm -rfv ${DEVICE_BUILD_ARCHIVE_PATH}
rm -rfv ${SIMULATOR_BUILD_ARCHIVE_PATH}

# 実機向けの`.xcarchive`をビルド
xcodebuild archive \
    -project "${DEVICE_BUILD_PATH}" \
    -scheme "UnityFramework" \
    -destination="iOS" \
    -archivePath "${DEVICE_BUILD_ARCHIVE_PATH}" \
    -sdk iphoneos \
    SKIP_INSTALL=NO \
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES

osascript -e 'display notification "Success Build UnityFramework-Device.xcarchive" sound name "Bip"'

# シミュレーター向けの`.xcarchive`をビルド
xcodebuild archive \
    -project "${SIMULATOR_BUILD_PATH}" \
    -scheme "UnityFramework" \
    -destination="iOS Simulator" \
    -archivePath "${SIMULATOR_BUILD_ARCHIVE_PATH}" \
    -sdk iphonesimulator \
    SKIP_INSTALL=NO \
    BUILD_LIBRARY_FOR_DISTRIBUTION=YES

osascript -e 'display notification "Success Build UnityFramework-Simulator.xcarchive" sound name "Bip"'

```


### ▼ 3. `.xcarchive`から`.xcframework`をビルド

前の手順でビルドした`.xcarchive`には`UnityFramework.framework`が含まれているので、**それらを以下のコマンドから`UnityFramework.xcframework`にビルドします。**
(この手順はそんなにビルド時間は掛からないはず)

```shell:build_for_xcframework.command
# 3. `.xcarchive`から`.xcframework`をビルド

XCFRAMEWORK_PATH="${OUTPUT_PATH}/UnityFramework.xcframework"

# 前回のビルド結果が残っている場合には先に削除
rm -rfv ${XCFRAMEWORK_PATH}

# 2の手順でビルドした実機&シミュレーター向けの`.xcarchive`から`.xcframework`をビルド
xcodebuild -create-xcframework \
    -framework "${DEVICE_BUILD_ARCHIVE_PATH}/Products/Library/Frameworks/UnityFramework.framework" \
    -framework "${SIMULATOR_BUILD_ARCHIVE_PATH}/Products/Library/Frameworks/UnityFramework.framework" \
    -output ${XCFRAMEWORK_PATH}

osascript -e 'display notification "Success Build UnityFramework.xcframework" sound name "Bip"'
```


### ▼ 4. `[BUILD_LIBRARY_FOR_DISTRIBUTION]`を有効にしている都合で、このままだと`UnityFramework.xcframework`を組み込んだ際にエラーが発生するので対策

前の手順で`UnityFramework.xcframework`が作られるので終わりかと思いきや...<font color="Red">現状のままだとこれを組み込んでもビルドエラーが発生します。</font>

原因について調べてみた所、以下のフォーラムが参考になりました。

- [Xcode 11 XCFramework "... is not a member type of ..." error](https://developer.apple.com/forums/thread/123253)

どうやら「2の手順」で`.xcarchive`をビルドする際に`[BUILD_LIBRARY_FOR_DISTRIBUTION]`と言うオプションを有効にしているのが影響してフォーラムで挙げられているようなエラーが出ているみたいでした。

なので今回はワークアラウンドとして上記のフォーラムで提案されている解決策を採用し、以下のように「`.xcframework`に含まれる`*.swiftinterface`を全検索しつつ、sedコマンドで`UnityFramework.`から始まる文字列を空文字に置換する」処理を挟むようにしてます。


```shell:build_for_xcframework.command
# 4. [BUILD_LIBRARY_FOR_DISTRIBUTION]を有効にしている都合で、このままだと`UnityFramework.xcframework`を組み込んだ際にエラーが発生するので対策
# 参考: https://developer.apple.com/forums/thread/123253

cd ${XCFRAMEWORK_PATH}

# エラー対策として、`.xcframework`に含まれる`*.swiftinterface`を全検索しつつ、sedコマンドで`UnityFramework.`を空文字に置換する
find . -name "*.swiftinterface" -exec sed -i -e 's/UnityFramework\.//g' {} \;

# `.swiftinterface-e` と言うファイルが出来るので削除
find . -name "*.swiftinterface-e" -exec rm -f {} \;

osascript -e 'display notification "Fix \".swiftinterface\" in xcframework" sound name "Bip"'

return 0
```

:::note warn
このフローについてはネイティブプラグインの実装言語や内容によって変わってくるかもしれない点に注意。
(少なくとも今回のサンプルプロジェクトに含まれているプラグインだと必要な模様)

こちらについても若干知識に自信が無い所となるので...もし間違っていたりしたらコメントなりで教えて頂けると幸いです。。 :bow: 
:::


#### 【補足】 `[BUILD_LIBRARY_FOR_DISTRIBUTION]`について

こちらのオプションを付けている意図についても軽く補足しておくと、こちらはSwiftの **「Module Stability」を有効にするために設定してます。**
この**Module Stability**と言うのは「**異なるバージョンのコンパイラから生成されたモジュール(フレームワーク)をインポート出来るようにする為の機能**」であり、有効にすると内部的に`*.swiftinterface`と言うファイルが生成されるようになります。
(その上で`XCFramework`化を行う上では必要っぽい)

- 参考
    - [ABI Stability とは？](https://yamatooo.blog/entry/2020/10/08/083000)
    - [Binary Frameworks in Swift - WWDC19](https://developer.apple.com/videos/play/wwdc2019/416/)
    - [ABI Stability なのになぜ Module compiled with Swift 5.1 cannot be imported by the Swift 5.1.2 というエラーが起こるのでしょうか？](https://ja.stackoverflow.com/questions/60498/abi-stability-%E3%81%AA%E3%81%AE%E3%81%AB%E3%81%AA%E3%81%9C-module-compiled-with-swift-5-1-cannot-be-imported-by-the-swi)





# おわりに

まだ理解できていないところや知識不足な点は残りつつも、一先ずは冒頭で挙げた課題を解決することが出来ました。

特にUnityに於いては「Unity単体で使う分には」シミュレーター向けにビルドすことはあまり無い？印象[^5]がありますが、**UaaLとしてネイティブアプリに組み込む上ではシミュレーターで動かすことも多くなるかと思われたので、物によっては役立てられるのかなと言う所感はあります。**
→ 例えばUaaLをネイティブUIのパーツとして組み込んだ状態で、各機種/各OSバージョンごとのシミュレーターでレイアウトを確認したいときとか[^6]

ただ、途中でも警告した通りに **`XCFramework`化の手順はビルド時間が必然的に伸びてしまうので**、ここらについてはワークフローなどを踏まえた上で考えていく必要があるかもです。

[^5]: 自分の経験上だとほぼ使うことが無かった(実機オンリー)
[^6]: この性質からカメラと言った実機依存の機能を使う場合には利用が難しい説はある...


## Apple Silicon対応について

[更新履歴](#更新履歴)にも記載している通り、Apple Siliconの件については途中から追記する形となりましたが、動かしてみたら案の定とでも言うべきか...**Unityがシミュレーター向けにビルドした際に吐き出すバイナリが`x86_64`で固定されている影響か、「ビルドするにはRosettaを経由する必要がある」ことが分かりました...。**
(大体想像は付いていた...)

### 軽く粘ってみたが...難しそう..？

まぁ動くといえば動くので及第点には達したかな...と言う所感はありつつも、それでもパフォーマンスが気になるので、何とかならないかと軽く調査をしてみましたが...**結論から言えば現時点での調査範囲では難しそうでした..。**

参考程度に今回調査した内容を残しておきます。
(もしこれ以外にも進展があれば随時追記/更新していきます)

#### ▼ シミュレーター向けにビルドした方のXcodeProjectの設定を見直してみる

UnityがiOSビルドで出力するXcodeProjectは恐らくはシミュレーター向けに設定されているであろうから、これを実機向けに変えることによってビルドが通らないか？と言うアプローチです。

とりあえずはシミュレーター向けにビルドした`Unity-iPhone.xcodeproj`の「Build Settings」を見直しつつ、以下のように設定を変更してみました。

- **Architectures**
    - `Architectures` : x86_64 → **arm64**
    - `Base SDK` : iphonesimulator (SDK not found) → **iOS**
    - `Supported Platforms` : iphonesimulator → **iOS**

この状態でUaaLとして組み込んだネイティブアプリ側の方でビルドしてみたところ、変更前まで出ていた「`Precompile bridging header`のコンパイルエラー」は解消されたものの...**今度はUnityFrameworkのビルドプロセスにて、以下のリンクエラーが出るようになりました。**

```shell
Ignoring file (プロジェクトのパス)/UnityProject/Builds/UnityFramework/Libraries/libiPhone-lib.dylib, building for iOS Simulator-arm64 but attempting to link with file built for iOS Simulator-x86_64
Ignoring file (プロジェクトのパス)/UnityProject/Builds/UnityFramework/Libraries/libil2cpp.a, building for iOS Simulator-arm64 but attempting to link with file built for iOS Simulator-x86_64
Ignoring file (プロジェクトのパス)/UnityProject/Builds/UnityFramework/Libraries/baselib.a, building for iOS Simulator-arm64 but attempting to link with file built for iOS Simulator-x86_64

Undefined symbol: _RegisterModule_SharedInternals
Undefined symbol: (以下省略..)
```

どうやらUnityがビルド時に出力するライブラリ(上記で言う`libiPhone-lib.dylib`, `libil2cpp.a`, `baselib.a`)が「`Simulator-x86_64`」向けにビルドされている影響でリンクできずにエラーになっている模様...。

#### ▼ [余談] Apple Siliconが`arm64`ならiOS Simulatorも`arm64`で実行される → なら実機向けのライブラリ(arm64)を使えば行ける説？

<font color="Red">結論から言うと、これも行けません。</font>

確かにApple Siliconの場合にはiOS Simulatorも`arm64`で実行されるようになると言う認識ですが..**何故か同じ`arm64`でも「実機向けとシミュレーター向けで環境が別れている」のでビルドを通すことができません。**

仮に無理やり上述のUnityが出すライブラリ(`libiPhone-lib.dylib`, `libil2cpp.a`, `baselib.a`)を実機向けビルドで出力したものに差し替えたとしても、以下のようなリンクエラーが出てしまいます。[^7]

[^7]: ファイルを差し替えた上で「Build Phase」の設定を差し替えてやれば可能

```shell
Ld (長いので省略)

ld: in (プロジェクトのパス)/UnityProject/Builds/UnityFramework/Libraries/libil2cpp.a(CultureInfo_jpug7.o), building for iOS Simulator, but linking in object file built for iOS, for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
```

それにしても`libiPhone-lib`...シミュレーター向けだと「Dynamic Library(.dylib)」で出て、実機向けだと「Static Library(.a)」で出るんだ...まぁ実機向けもdylibだと審査通らないと思うのでそれはそう。。



## 続き(未定)

今回[サンプルプロジェクト](https://github.com/mao-test-h/UaaL-Examples-iOS)を作っていく中で幾つか話足りない点が出てきたので、元気があれば別途記事にて解説するかも...。
(それよりもMac Studioのセットアップ + この記事の補足のほうが先..)

具体的に言うと以下のような点があります。

- **iOSネイティブアプリ側でUaaL + SwiftUIを導入する際のtips**
    - 参考: [Unity 2020 Integration With SwiftUI](https://davidpeicho.github.io/blog/unity-integration-swiftui/)
- **UaaL(`UnityFramework`)の[sendMessageToGOWithName](https://docs.unity3d.com/Manual/UnityasaLibrary-iOS.html)を使わずに、ネイティブからUnityのメソッドを呼ぶ方法について**
    - 今回のサンプルだとSwiftUIで実装しているスライダーの値をUnityに渡すところでこれを行っている
    - 文字列以外(但しBlittable型)が渡せる上にポインタ芸が可能
        - → 例えば大きいデータのポインタを渡してmemcpyする方法など
- **SwiftUIのCanvas Previewに関する注意点**
    - <font color="Red">`UnityFramework.xcframework`を導入した状態でSwiftUIの`Canvas Preview`を表示しようとするとコンパイルエラーが発生するので、回避するためのワークアラウンドが必要な話</font>
    - → [こちらのソース](https://github.com/mao-test-h/UaaL-Examples-iOS/blob/feature/xcframework/XcodeProject/UaaLExample/UaaLExample/SwiftUI/UnityView.swift)にてコメントで補足してる内容