watchOSアプリの開発（プロジェクト作成〜Watch Connectivityを用いたiOSアプリとの通信処理）

## 概要

この記事は[フリューAdvent Calendar 2024](https://qiita.com/advent-calendar/2024/furyu)の8日目の記事となります。

ピクトリンク事業部開発部のnakaseです。主にモバイルアプリケーションの開発を担当しています。
業務とは異なるのですが、先日参加したSPAJAM（ハッカソン）本選にて、AppleWatchを使ったアプリを開発しました。
watchOSアプリの開発自体が初めてだったのですが、環境構築等に一部苦労しつつも1日でアプリとして動く形に仕上げる事ができ、大変良い経験となりました。
当日は勿論ドキュメントを残す余裕などなかったので、改めてwatchOSアプリの開発までの手順を振り返り、特にWatch Connectivityを用いたiOS/WatchOS間の通信処理実装についてまとめておきたいと思います。

## 準備
### プロジェクト作成
今回は、既存プロジェクトへの適用ではなく完全に新規でプロジェクトを作成することとします。
File->New->Projectから新規プロジェクト設定を立ち上げ、watchOSタブ->Appを選択します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/5b0928b7-ba17-224b-b611-57dcf4f5e56f.png" width=70%>

今回はiOSアプリと連携するWatchOSアプリを作成するため、`Watch App with New Companion iOS App`にチェックを入れました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/387cbfaf-db1a-efca-75f9-613a469a9bcf.png" width=70%>

iOS/WatchOSそれぞれのTARGETがプロジェクトに追加されていることが確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/7c2654f8-4fa9-86fa-effe-6436269f72d8.png" width=70%>


### シミュレータ実行
この状態でシミュレータによるHello,Worldアプリの起動確認ができます。
iOS/WatchOS各ターゲットのSchemeがプロジェクト生成時に作成されているので、それぞれ対象を任意のiPhone/WatchOSシミュレータにした状態でデバッグ実行すると、以下のようにシミュレータ上にHello,Worldが表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/a5d12c82-4206-1fff-fd6c-bc824f6fe40b.png" width=60%>

各OSで独立した機能についてはシミュレータのみでの実装・確認が可能ですが、OS間の通信を伴う機能を実装したい場合は後述の実機実行環境を整える必要があります。

### 実機実行
実機デバッグにはApple Developerへのデバイス登録並びにProfileの作成が必要となります。

Apple WatchのUDIDは、XCodeの`Devices and Simulators`から確認可能です。
ただし、接続済みのiPhoneとペアリングされている必要があります。
ペアリングは出来ている状態にも関わらず、対象のAppleWatchが見つからない/接続されない場合については、Apple Watchを再起動することで解決しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/2bfbcde7-876b-90df-b613-ca942dc2be7c.png" width=70%>

また、AppleWatchを初めて開発用として利用する場合は、設定アプリからプライバシーとセキュリティを選択し、デベロッパーモードをONにする必要があります。
（当初デベロッパーモードが設定項目として表示されない事象があったのですが、こちらも結局AppleWatchを再起動したら表示されるようになりました。接続不安定な時はとりあえず再起動するのが良さそうです...）

UDIDの確認さえ済んでしまえば、あとは通常のiOSアプリと同様BundleIDの登録やProvisioning Profileの作成を実施し、ターゲットのSigninにProfileを適用すれば作業は完了します。

## iOS/WatchOS間通信

### Watch Connectivity
[Watch Connectivity](https://developer.apple.com/documentation/watchconnectivity)とは、iOSアプリとペアリングされたwatchOSアプリの双方向通信を実現するためのフレームワークです。
シンプルな実装で、データのやりとりを実装する事ができます。

今回は[sendMessage(_:replyHandler:errorHandler:)](https://developer.apple.com/documentation/watchconnectivity/wcsession/sendmessage(_:replyhandler:errorhandler:))を使ってデータの送受信を実現します。
これは双方のアプリがフォアグラウンドの状態を前提として、即座にデータを送信するためのメソッドです。

なお、バックグラウンドでのデータ通信を行う際は、`sendMessage`ではなく[transferUserInfo(_:)](https://developer.apple.com/documentation/watchconnectivity/wcsession/transferuserinfo(_:))もしくは[transferFile(_:metadata:)](https://developer.apple.com/documentation/watchconnectivity/wcsession/transferfile(_:metadata:))を使う必要があります。（今回はこちらの説明は割愛します）

また、注意点なのですが、以下に実施するOS間通信の実装はシミュレータでは機能しないため、実機で動作確認を行う必要があります。

### iOS/watchOS間のデータ送受信実装

#### iOS(送信側）の実装

まず送信側の実装です。
UIはTextFieldに文字を入力し、ボタンタップ時にその送信処理を行うだけの簡単なものとします。
ViewModel側ではinit時に`WCSession.activate()`を行います。
そしてデータ処理を実行する`sendMessageToWatch(text: String)`メソッドでは、`WCSession.sendMessage`を利用し、WatchOSアプリにテキストデータを送る処理を実装しています。
データ型は[String : Any]のDictionaryで、複数のkey-valueをまとめて送ることが可能です。

```swift

final class phoneAppViewModel: NSObject, ObservableObject {

    private let session: WCSession = .default

    override init() {
        super.init()
        if WCSession.isSupported() {
            let session = WCSession.default
            session.activate()
        }
    }
    
    func sendMessageToWatch(text: String) {
       if WCSession.default.isReachable {
            WCSession.default.sendMessage(["text": text]){ response in
                print("sent successfully: \(response)")
            } errorHandler: { error in
                print("Error sending image: \(error.localizedDescription)")
            }
        }
    }
}

struct ContentView: View {
    @StateObject private var viewModel = phoneAppViewModel()
    @State private var message: String = ""
    
    var body: some View {
        VStack {
            TextField("Enter message", text: $message)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            Button(action: {
                viewModel.sendMessageToWatch(text: message)
            }) {
                Text("Send to Watch")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
            
        }
        .padding()
    }
}

```

なお、Watch側との通信が確立されていない場合、`WCSession.isReachable`がfalseになるため送信ができません。
デバッグ時においては、iOS/watchOSの各ターゲットが個別にデバッグ実行されている必要があります。

#### watchOS(受信側）の実装

続いて受信側の実装です。
UIはシンプルに、受信した文字を表示するだけです。
受信側のViewModelでは、WCSessionDelegateの`session(_ session: WCSession, didReceiveMessage message: [String : Any])`を定義します。
このメソッドでは、送信側から`sendMessage`で送られてきたデータを受信し（引数：`message`）、必要なオブジェクトを抽出して`receivedText`に代入しています。
View側ではこの値をObserveし、変化があった時にテキスト表示を更新するよう実装します。
（なお、init時にactivateを実行するのは送信側と共通です）

```swift
class WatchAppViewModel: NSObject, ObservableObject, WCSessionDelegate {
    
    @Published var receivedText: String = "Waiting..."

    override init() {
        super.init()
        if WCSession.isSupported() {
            let session = WCSession.default
            session.delegate = self
            session.activate()
        }
    }
    
    func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
        DispatchQueue.main.async { [self] in
            if let text = message["text"] as? String {
                DispatchQueue.main.async {
                    self.receivedText = text
                }
            }
        }
    }
}

struct ContentView: View {
    
    @StateObject private var viewModel = WatchAppViewModel()
    
    var body: some View {
        VStack {
            Text("Received Message:")
                .font(.headline)
            
            Text(viewModel.receivedText)
                .font(.body)
                .padding()
                .multilineTextAlignment(.center)
        }
        .padding()
    }
}
```

### 結果
上記のようにWatch Connectivityを利用した送受信処理の実装を行うことで、iOSアプリから送信したデータをWatchアプリで受信し、UIが即時更新される機能が実現できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957449/0864f2df-14ea-a83d-d772-77307d7f3e93.png)



### WatchOSアプリ -> iOSアプリの通信について
今回はiOSアプリからWatchOSアプリへデータを送信する処理について説明しましたが、Watch Connectivityは双方向通信フレームワークなので、逆方向の通信ももちろん可能です。
詳細説明は割愛しますが、WatchOS側から`WCSessions.sendMessage`を画面タップなどのタイミングで実施し、iOS側で`WCSessionDelegate`を実装することで同様に実現できます。

## おわりに
iOSアプリと連動するWatchOSアプリのプロジェクト作成・実行方法、並びにペアリング済みの端末間でのWatch Connectivityを用いた通信についてまとめました。
実機デバッグ時にApple Watchの認識や通信確立がうまくいかずに手間取ったりはしましたが、実装に関してはとてもシンプルで、軽量データの即時通信であれば簡単に実装できることが分かりました。

ただ、Watch Connectivityの章でも記載した通り、`sendMessage`での送受信のみ実装したため、どちらかのアプリがバックグラウンドにある状態ではデータ通信が行えません。
また画像などの重いデータを送る際にもsendMessageは利用できないため、別のメソッドを使用する必要があります。
WatchOSによる行動履歴やiOS側の登録情報を利用し、より機能的なWatchOS対応アプリを開発するにはデータ通信周りの処理についてもっと理解を深めていく必要があると感じました。

個人開発用のApple Watchを手に入れたので、今後色々と試していければと思います。