MAUI AppをiOSの実機&Simulatorで実行するスクリプトを作った話

.NET MAUI、みなさん使っていますか?

色々バグは多いですが、Xamarinを少し触ってた身からすると一つのプロジェクトで済むのはとても楽でありがたく使っています。

さて、一時期、Visual Studio for MacでiOS向けビルドを最新OSでしか実機デバッグできないバグがありました。
また、それ以外にも「最新のシミュレータしかリストに表示されない」バグもありました。

もちろんコマンドを打てば実行は可能ですが、いちいちコマンドを打ち込むのはめんどくさい… ということで、スクリプトを作成しました。

:::note warn
Xcodeやdotnet、MAUI等がセットアップ済みなMac環境での実行を想定しています。
Xcodeに付属するツールを使用しているため、LinuxやWindowsでは実行できません。
:::

## TL;DR

- **コード読め** -> https://gist.github.com/TetsuOtter/15f18a1bd4b0212a7bca205f0bdccd00
- デバイスでの実行コマンドが長すぎたため、スクリプト化した
- SimulatorデバイスのUDIDを覚えるのが面倒だったので、リストから選択できるようにした
- デバイスの列挙には時間がかかったため、デバイス名/UUID直接指定でも実行できるようにした
- ビルド設定も変更できるようにした

```sh:シミュレータで実行するときのdotnetコマンド例
dotnet build -t:Run SampleProject/SampleProject.csproj -f net8.0-ios -r iossimulator-x64 --no-self-contained --nologo "/p:_DeviceName=:v2:udid=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
```

```sh:実機で実行するときのdotnetコマンド例
dotnet build -t:Run SampleProject/SampleProject.csproj -f net8.0-ios -r ios-arm64 --no-self-contained --nologo "/p:_DeviceName=SampleRealDevice"
```

## 機能/実現手法

### デバイス列挙機能

あなたは、iOSシミュレータのUDIDを全て覚えていますか?
流石に全部覚えてる人はいないと思います。

そこで、シミュレータ (と、ついでに接続されているデバイス) を一覧化して、選択して実行できるようにしました。

使用可能なデバイスの一覧は、次のコマンドで取得できます (実行完了に少し時間がかかります)

```sh:shell
xcrun xctrace list devices
```

出力は、次のような感じです。

```log:exec log
== Devices ==
SampleHostMac (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
SampleRealDevice (16.6) (XXXXXXXX-XXXXXXXXXXXXXXXX)

== Simulators ==
iPad (10th generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
iPad Air (5th generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
iPad Pro (11-inch) (4th generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
iPad Pro (12.9-inch) (6th generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
iPad mini (6th generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
iPhone 14 Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
iPhone 14 Plus Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
iPhone 14 Pro Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
iPhone 14 Pro Max Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
iPhone SE (3rd generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
```

`Devices` が実機、`Simulators` がシミュレータです。そのままですね。
デバイス名、OSバージョン、UDID (UUID) が一行に表示されています。

これを元に、デバイスを列挙します。
whileループで、インデックスとともにデバイス情報を出力してあげます。
出力結果は、次のようになります。

```log:exec log
== Devices ==
[0] SampleHostMac (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
[1] SampleRealDevice (16.6) (XXXXXXXX-XXXXXXXXXXXXXXXX)

== Simulators ==
[2] iPad (10th generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
[3] iPad Air (5th generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
[4] iPad Pro (11-inch) (4th generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
[5] iPad Pro (12.9-inch) (6th generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
[6] iPad mini (6th generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
[7] iPhone 14 Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
[8] iPhone 14 Plus Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
[9] iPhone 14 Pro Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
[10] iPhone 14 Pro Max Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
[11] iPhone SE (3rd generation) Simulator (16.4) (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
```

単にインデックスを追加しただけですね。
本当は適切なPaddingを追加… とかしたかったのですが、文字列解析が面倒だったのでやめました。

あとは、ユーザからインデックスの入力を受けて、それに対応したデバイス情報を変数に格納して次に渡します。

### 実機 / Simulator識別

dotnetコマンドでアプリを実行させる際、DeviceNameに指定するのは

- シミュレータの場合はUDID
- 実機の場合は実機のデバイス名

になります。

そして、Runtime指定でも、

- シミュレータの場合は `-r iossimulator-x64`
- 実機の場合は `-r ios-arm64`

になります。

今回のスクリプトでは、実機でもシミュレータでもどちらでも実行できるようにしたかったため、この識別機能を実装しました。

…とは言っても、

- UUID形式の文字列が含まれていればであればシミュレータ
- そうでなければ実機

という実装をしているだけです。

### dotnetコマンド実行

無事にデバイスの選択が完了したら、dotnetコマンドの実行に移ります。

スクリプト内の記述は、次のようになっています。

```sh:shell
dotnet build -t:Run $TARGET_PROJ -f $TARGET_FRAMEWORK -r $RUNTIME_IDENTIFIER --self-contained --nologo "/p:_DeviceName=$DeviceName" ${ARGS[@]}
```

`TARGET_PROJ` と `TARGET_FRAMEWORK` は、他のプロジェクトでも使用できるようにするために変数化したものです。

`RUNTIME_IDENTIFIER` は、実機とSimulatorで違うRuntime指定を行うために使用しています。

今回のスクリプトの一番の肝であるデバイス指定について、
コマンドでは、デバイスの指定を `/p:_DeviceName=***` で行います。
実機の場合は単にそのデバイスの名前を指定するだけなのですが、
シミュレータの場合はデバイス名ではなくUDIDを使用し、しかもUDIDも `:v2:udid=` みたいなprefixをつけなくちゃいけなくて、少し面倒でした。

## 実行の様子

iOS15.4とiOS16.4のシミュレータの二つをインストールしてあるのでリストが長くなっていますが、通常は十数個で収まるのではないかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687506/a69cafcc-1984-92da-ca16-cefae72fddfb.png)

また、デバイス名やUDIDの直指定を行うことで、リストを表示することなく実行が可能になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687506/88febfcb-dd12-6d70-5c50-73d1a3de7510.png)


## 感想

バグでVisual Studioから実行できない期間中はもちろん必須ツールでしたが、それ以外の期間でも、時間のかかるVisual Studioの起動をスキップできたのは大きかったです。
また、複数デバイスを切り替えながらテストしたいときなどに、いちいちVisual StudioのGUIでデバイスを選択する作業が必要なくなり、かなり楽になりました。

このスクリプトを作ったの自体去年の7月で色々忘れてきてて、記事を書くにあたってソースを全然出せず、その点については申し訳ないです…

## 課題

現状は単にビルドして実行するのみで、デバッグはできません。
ブレークポイントを張ったとしても、そこで止まってくれるはずもなく。
VSCodeと接続してデバッグできたら便利なんでしょうけど、とてもとても面倒そうなので今回は諦めました。

また、シミュレータで実行したいと考えたとき、UUIDを覚えるか、リストから探さなくちゃいけないというのも不便です。
デバイス名 (`iPad Pro` 等) で絞り込んで表示できても良かったかもですね。
絞り込んで一つに特定できたら、リストを経由せずにそのまま実行とか。


...というか、本当はdotnet toolsで実現したかったんですよね。それはそれで面倒そうだったので、慣れてるシェルスクリプトで書いちゃいましたが。
そのうち、やる気が起きたら、C#で書き直しちゃいたいです。そしたらnugetで簡単に入れることができますし。