コマンドラインでSimulatorを起動してDebug用のアプリをインストールする

コマンドラインから iOS の Simulator を起動して、アプリをインストールしたいことがあったので、その方法を調査した内容を記事にしました。

## 調査時の環境

- Xcode 16.3
- macOS 15.4.1

## 方法

単純に Simulator をコマンドラインから Simulator を起動して、アプリをインストールしたいだけならちょっと調べてたら、たくさん記事が出てきましたので、以下を参考にさせていただきました。

https://qiita.com/hituziando/items/3d1205140170932c49e7

方法は簡単で、以下手順で行えます。

### 1. アプリをビルド

まずは`xcodebuild`コマンドで、インストールしたいアプリをビルドします。
ポイントは、`-derivedDataPath`で DerivedData の生成先を指定するところです。
アプリインストールを実行するときに、DerivedData の中からアプリの実行可能ファイルを使うので、DerivedData の場所がわかるように、あらかじめ指定しておきます。

コマンド例

```console
xcodebuild \
  -projec hoge.xcprojc \
  -scheme hoge \
  -configuration Debug \
  -destination "platform=iOS Simulator,OS=18.1,name=iPhone 16 Pro" \
  -derivedDataPath ＜ビルドの成果物の格納先のパス＞
  build
```

::: note
目的はDerivedDataを生成することなので、`xcodebuild`にこだわる必要はなしです。
`fastlane`の`scan`でも同じことができるはずですので、`xcodebuild`コマンドを使った方法はあくまで一例です。
:::

### 2. Simulator の起動

続いて、`xcrun simctl`コマンドで Simulator を起動します。

コマンド例

```console
xcrun simctl boot ＜デバイス名 or SimulatorのUDID＞
```

::: note info
ビルドと Simulator の起動は順番が前後しても問題ありません。
Simulator はコマンド実行完了後も Simulator がすぐ使える状態ではないこともあるので、ビルドも前に実行しておく方が全体の実行時間は下がりそうです。
特に事情がなければさきに Simulator の起動を実行しておいた方が良いでしょう。
:::

### 3. Simulator にアプリをインストール

最後に`xcrun simctl`コマンドで Simulator へアプリをインストールします。

コマンド例

- 複数の Simulator が起動していて、特定のデバイスにインストールしたい場合

```console
xcrun simctl install ＜デバイス名 or SimulatorのUDID＞ ＜DerivedDataのパス/Build/Products/Debug-iphonesimulator/hoge.app＞
```

- 起動している Simulator のどれかにアプリをインストールする

```console
xcrun simctl install booted ＜DerivedDataのパス/Build/Products/Debug-iphonesimulator/hoge.app＞
```

:::note info
`booted`を指定すると、起動している Simulator を自動で選択してくれます。
CI 環境などで Simulator がひとつしか起動していないことがわかっている場合も、`booted`を指定した方が簡潔で良さそうです。

以下 simctl コマンドの help コマンドの説明を引用

> For subcommands that require a <device> argument, you may specify a device UDID
> or the special "booted" string which will cause simctl to pick a booted device.
> If multiple devices are booted when the "booted" device is selected, simctl
> will choose one of them.
:::

## 起動したい Simulator のバージョンを指定したいときはどうする？

私のユースケースとして、Github Actions の CI 環境で決まった Simulator に対して Maestro を使って E2E テストを行うというものだったため、アプリインストール済みの Simulator を事前に起動しておきたかったです。
E2E テスト内でスクリーンショットも撮ったりするので前提として、実行する Simulator の種類、iOS バージョンは固定しておきたかったです。

`xcrun simctl boot`コマンドで Simulator を起動するときに、バージョンまで指定したい場合は、UDID による指定しかなさそうでした。
ただ、Github Actions の macOS ランナーでは Simulator の UDID が変わってしまうことがあるので、固定で UDID を指定すると UDID が変わったときにワークフローが壊れてしまう懸念もありました。

そこで、`xcrun simctl list`で Simulator のデバイス名、バージョン、UDID を一覧出力して、そこから指定のデバイスの UDID を動的に取得する方法で対応することにしました。
そうすれば、Simulator の UDID が勝手に変わろうとも、動的に取得するのでワークフローが壊れることはありません。

以下のようなスクリプトを作成し、指定したデバイス名、バージョンの合致する Simulator を起動するようにしました。

```console
#!/bin/bash

DEVICE_NAME="$1"
IOS_VERSION="$2"

if [ -z "$DEVICE_NAME" ] || [ -z "$IOS_VERSION" ]; then
  echo "Usage: $0 \"<Device Name>\" \"<iOS Version>\""
  exit 1
fi

# フラグ用変数
FOUND_SECTION=0
UDID=""

# 1行ずつ読み取り
while IFS= read -r line; do
  # セクション（-- iOS XX --）の検出
  if echo "$line" | grep -q -- "-- $IOS_VERSION --"; then
    FOUND_SECTION=1
    continue
  fi

  # 他のバージョンのセクションに入ったら終了
  if echo "$line" | grep -q -- "^-- iOS " && ! echo "$line" | grep -q -- "$IOS_VERSION"; then
    if [ "$FOUND_SECTION" -eq 1 ]; then
      break
    fi
  fi

  # セクション内でデバイス名を探す
  if [ "$FOUND_SECTION" -eq 1 ]; then
    if echo "$line" | grep -q "$DEVICE_NAME"; then
      UDID=$(echo "$line" | grep -oE '[A-F0-9\-]{36}')
      break
    fi
  fi
done < <(xcrun simctl list devices available)

# UDIDが見つかったかチェック
if [ -z "$UDID" ]; then
  echo "No matching device found for \"$DEVICE_NAME\" on $IOS_VERSION"
  exit 1
fi

echo "Found UDID: $UDID"

# 起動
xcrun simctl boot "$UDID"

echo "Simulator booted successfully."
```

以下のように使用すれば、指定したデバイスが起動できるようになります。

```console
bash boot_simulator.sh "iPhone 16 Pro" "iOS 18.1"
```

## おわり

以上が、コマンドラインから Simulator へアプリをインストールする方法でした。
`xcrun simctl boot`でもバージョン指定できるものと思っていましたが、いくら調べてもなさそうだったので、一手間加えなきゃでしたが、実行方法さえ思いつけば ChatGPT などが簡単にスクリプトを組んでくれるので、便利な世の中になったなーと思いました！