iPhone / Apple Watchシミュレータを手動でペアリングして通信させる

Xcodeで提供されるiPhoneシミュレータ、Apple Watchシミュレータは、一部がデフォルトでペアリング済みになっています。

本稿では、ペアリングされていないシミュレータを手動でペアリングして相互に通信させる方法を説明します。デフォルトではペアリングされていない機器の通信検証に役立てばと思います。

開発環境はXcode 16.2を使用しています。

## ペアリング方法 (TLDR)

以下のコマンドを実行すればペアリング完了です。

```console
$ simctl pair <watch device id> <phone device id>
```

以上!

## 補足

### コマンドに指定するデバイスIDを特定する

まずは、`simctl pair` コマンドに指定する `<watch device id>` `<phone device id>` を特定する必要があります。

ターゲット毎のシミュレータのデバイスIDは、`xcrun simctl list devices`で取得できます。以下がコマンドの実行例であり、機種名の後ろに括弧で書かれたUUIDがデバイスIDです。このリストからペアリングしたい機器のIDをピックアップします。

```console: (大量に出力されるので一部のみ抜粋)
$ xcrun simctl list devices
== Devices ==
-- iOS 17.0 --
    iPhone 15 Pro (71694FC1-B323-476C-8EEB-B271E0772DB6) (Shutdown) 
    iPhone 15 Pro Max (262BC2CB-3D68-47DE-A01D-77CA3914BB81) (Shutdown) 
    iPhone 15 (3D5DF7A0-B2AE-4AC0-8980-9A2E34F6AE3F) (Shutdown) 
(...略...)
-- iOS 18.2 --
    iPhone 16 Pro (FAECD964-C876-4586-93EB-CC201DE2BB5E) (Booted) 
    iPhone 16 Pro Max (434D029F-F169-47BD-9C29-C1B67F3C7C45) (Shutdown)
(...略...)
-- watchOS 10.5 --
    Apple Watch Series 9 (45mm) (53F2D5FE-B906-40C9-833B-DBA11F97D9A6) (Shutdown) 
    Apple Watch Series 9 (41mm) (23795EEA-419F-406A-84DA-F3625A815917) (Shutdown) 
    Apple Watch Ultra 2 (49mm) (A863F57A-A8E0-4A6C-B6DA-A0346DE3411E) (Shutdown) 
(...略...)
```

以降、以下の2機種をペアリングする例を説明します。
(少し古い世代の機器ですが、新しい機種は既にペアリング済みのためご容赦ください。)

| 機器 | デバイスID |
|-----|-----------|
| iOS 17.0 / iPhone 15 Pro | 71694FC1-B323-476C-8EEB-B271E0772DB6 |
| watchOS 10.5 / Apple Watch Series 9 (45mm) | 53F2D5FE-B906-40C9-833B-DBA11F97D9A6 |


### ペアリングする

冒頭に記載したコマンドを実行しますが、改めてシンタックスは `simctl pair <watch device id> <phone device id>` です。

以下が、実行例です。

```console
$ xcrun simctl pair 53F2D5FE-B906-40C9-833B-DBA11F97D9A6 71694FC1-B323-476C-8EEB-B271E0772DB6
D57DC8AB-8144-4F6F-A906-585A5498C748
```

:::note warn
コマンド引数は、Watchの機器IDから先に指定する点にご注意ください。
:::

コマンドが成功すると、ペアIDが払い出されます (実行例では、D57DC8AB-8144-4F6F-A906-585A5498C748)。

### ペアリングされたことを確認する

ペアリングされたことの確認は、`xcrun simctl list pairs` コマンドで確認できます。
以下が、実行例です。3行1組で、ペアID、WatchデバイスID、iPhoneデバイスIDの組み合わせが出力されています。先頭のエントリが先ほどペアリングした情報です。正しくペアリングできたことが確認できました。

```console
$ xcrun simctl list pairs
== Device Pairs ==
D57DC8AB-8144-4F6F-A906-585A5498C748 (active, disconnected)
    Watch: Apple Watch Series 9 (45mm) (53F2D5FE-B906-40C9-833B-DBA11F97D9A6) (Shutdown)
    Phone: iPhone 15 Pro (71694FC1-B323-476C-8EEB-B271E0772DB6) (Shutdown)
21FD00BD-0694-4B00-837D-BE03832CDBE8 (active, disconnected)
    Watch: Apple Watch Series 10 (42mm) (3B843C81-3BAF-43C0-867A-C50F1D156740) (Shutdown)
    Phone: iPhone 16 (0FC02D84-5189-4345-B867-0D545EACFC39) (Shutdown)
EB4AF6D1-E0AF-48FD-942D-034438CF77AC (active, disconnected)
    Watch: Apple Watch Ultra 2 (49mm) (40243C10-5730-4D67-AB29-9D783F85E529) (Shutdown)
    Phone: iPhone 16 Pro Max (6B971827-3B5F-4719-A9E2-66E4EB93F4B3) (Shutdown)
14D33CB6-D5AE-401C-855D-8840CDD3F074 (active, disconnected)
    Watch: Apple Watch Series 10 (46mm) (6A21A8F5-EE21-4315-831D-04C4BF791984) (Shutdown)
    Phone: iPhone 16 Pro Max (434D029F-F169-47BD-9C29-C1B67F3C7C45) (Shutdown)
AAA3C07F-C86E-49B6-892C-6CFB92F86059 (active, disconnected)
    Watch: Apple Watch Series 10 (42mm) (1708EE29-E020-4E8B-B9A6-BF4B56EA6438) (Shutdown)
    Phone: iPhone 16 (22C904C3-9E7F-4273-A43C-9FB09A305961) (Shutdown)
F0960AA6-71E0-4DCF-ABC5-46AA4AA46363 (active, disconnected)
    Watch: Apple Watch Ultra 2 (49mm) (51D39B4D-2795-4550-9A27-7AB2C78F1C23) (Shutdown)
    Phone: iPhone 16 Plus (E75DC61C-1F1F-4696-A1D0-F40773DAFD6A) (Shutdown)
0B3EAFB3-C862-48D0-B95E-9E175AB05870 (active, connected)
    Watch: Apple Watch SE (40mm) (2nd generation) (77EAE8FA-51CC-4CA0-A411-A76E85E561BF) (Booted)
    Phone: iPhone 16 Pro (FAECD964-C876-4586-93EB-CC201DE2BB5E) (Booted)
```

## 通信させてみる

以下はデバイス間の疎通確認を行うためのサンプルアプリです。全く面白味の無いサンプルですが、以下の挙動をします。
1. iPhone, WatchどちらかでSendボタンを押すと、現在時刻を対向機器に送信する
2. 受信側の対向機器は、受信した時刻データと累積受信回数 (Count) を画面に表示する

iPhone, Watchで完全に同一のコードを共有しており、同じように動作します。

![app-running.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31469/2078c95f-3017-4671-91f2-6436602a7fb5.gif)


## コードサンプル

上記のサンプル通信アプリのコードです。ソースファイルは、iOS, watchOS両方合わせて以下の1ファイルのみです。

```swift
import SwiftUI
import WatchConnectivity

final class WCSessionModel: NSObject, WCSessionDelegate, ObservableObject {
  @Published var count = 0
  @Published var time = Date(timeIntervalSince1970: 0)

  override init() {
    super.init()
    WCSession.default.delegate = self
    WCSession.default.activate()
  }

  @MainActor
  func onTimeReceived(_ time: Date) {
    self.count += 1
    self.time = time
  }
  /// ペアリング相手から受信したメッセージのハンドラ
  func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
    let time = message["time"] as! Date
    Task { await self.onTimeReceived(time) }
  }

  // 以下は、WCSessionDelegateプロトコルに必要なメソッド。サンプルでは使用しないので空実装。
  func session(
    _ session: WCSession,
    activationDidCompleteWith activationState: WCSessionActivationState,
    error: (any Error)?
  ) {}
  #if os(iOS)
    func sessionDidBecomeInactive(_ session: WCSession) {}
    func sessionDidDeactivate(_ session: WCSession) {}
  #endif
}

/// View
struct ContentView: View {
  @ObservedObject var wcSessionModel = WCSessionModel()
  var body: some View {
    VStack {
      Text("Count: \(wcSessionModel.count)")
      Text("\(Date.ISO8601FormatStyle().format(wcSessionModel.time))")
      Button("Send") {
        WCSession.default.sendMessage(["time": Date.now], replyHandler: nil)
      }
    }
  }
}

@main
struct WCExampleApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}
```

iOSとwatchOSでのコード共有は、Xcode右端のファイルインスペクターから、Target MembershipにiOSおよびwatchOSのターゲットを両方追加することで可能になります。

![target-members.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31469/84cc2ac8-f778-4936-b32c-08cf97a62fb2.png)