Avance.Lab

技術紹介

iPhoneとApple Watchで双方向通信(Watch Connectivity)

公開日:2023.08.25 更新日:2023.08.25

tag: IoTスマートデバイス

こんにちは、n/aです。

iPhone と Apple Watch の双方向通信は、Watch Connectivity を使うと少ないコードで実装できます。

今回は、簡単なカウンターアプリの作成を通して Watch Connectivity の実装方法をご紹介します。
iPhone と Apple Watch でカウンターの値を共有し、双方から値を変更できるようにしていきます。

開発環境

  • Xcode 14
  • SwiftUI
  • iPhone 11 Pro (iOS 16.3.1)
  • Apple Watch Series 7 (watchOS 9.3.1)

実装

0. 事前準備

  • iPhone の Bluetooth と Wi-Fi を ON にする。
  • iPhone と Apple Watch をペアリングする。

1. プロジェクト作成

今回は iPhone アプリと Apple Watch アプリをセットで新規作成します。

Xcode のプロジェクト新規作成ウィザードで以下を選択します。

  • Platform: watchOS
  • Application: App

Product Name、Organization Identifer を入力し、 Watch App with New Companion iOS App を選択します。

iPhone アプリと Apple Watch アプリがペアになったプロジェクトが作成されました。

2. Watch Connectivityの初期化

まずは Watch Connectivity での通信が可能な状態にしていきます。以下の内容を実装します。

  • WCSession を作成する
  • WCSessionDelegate を設定する
  • WCSession.activate() を呼ぶ

WCSessionDelegate の実装も含めて iPhone 側は以下のようになります。
(ここでは ViewModel に実装しました。)

import WatchConnectivity

final class PhoneContentViewModel: NSObject, ObservableObject {

    private let session: WCSession

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

extension PhoneContentViewModel: WCSessionDelegate {
    
    func sessionDidBecomeInactive(_ session: WCSession) {
        
    }
    
    func sessionDidDeactivate(_ session: WCSession) {
        
    }
    
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        
    }
}

Apple Watch側のソースコードです。ほとんど同じですが、sessionDidBecomeInactive(_ session: WCSession) , sessionDidDeactivate(_ session: WCSession) は iPhone 専用メソッドなので実装不要です。

import WatchConnectivity

final class WatchContentViewModel: NSObject, ObservableObject {

    private let session: WCSession

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

extension WatchContentViewModel: WCSessionDelegate {
    
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        
    }
}

3. カウンター画面作成

今回は iPhone ・ Apple Watch 共に、カウント増減ボタンと値表示のみのシンプルな画面とします。

以下は iPhone のソースコードです。Apple Watch は PhoneContentViewModel を WatchContentViewModel に置き換えてください。

import SwiftUI

struct ContentView: View {
    
    @ObservedObject private var viewModel =  PhoneContentViewModel()
    
    var body: some View {
        VStack {
            Button(action: {
                viewModel.countup()
            }) {
                Image(systemName: "arrowtriangle.up.circle.fill")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 80.0, height: 80.0)
                    .foregroundColor(.pink)
            }
            .buttonStyle(.plain)
            Text(String(Int(viewModel.counter)))
                .font(.system(size: 64.0))
                .bold()
            Button(action: {
                viewModel.countdown()
            }) {
                Image(systemName: "arrowtriangle.down.circle.fill")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 80.0, height: 80.0)
                    .foregroundColor(.blue)
            }
            .buttonStyle(.plain)
        }
        .padding()
    }
}

カウントデータと増減処理はViewModelに実装します。これも iPhone ・ Apple Watch 共通です。

final class PhoneContentViewModel: NSObject, ObservableObject {

    ...

    @Published var counter = 0
    
    func countup() {
        counter += 1
    }
    
    func countdown() {
        counter -= 1
    }
}

これで iPhone ・ Apple Watch それぞれのカウンターの値を変更できるようになりました。

続いてカウンター値を相手に送信する処理を追加します。

4. データ送信

Watch Connectivity の送信メソッドはいくつかありますが、今回は sendMessage(_:replyHandler:errorHandler:)transferUserInfo(_:) を使用します。

2つのメソッドの主な違いは、

  • sendMessage は即時送信され、必要に応じて応答やエラーを受け取ることができますが、バックグラウンド送信は非対応です。
  • transferUserInfo はバックグラウンド送信対応で、送信データはキューに入れられ、送信可能になった時点で送付されます。

Apple Watch はしばらく操作していないと一時的に通信不能な状態になることがありますので、今回は iPhone から Apple Watch への送信は transferUserInfo を使用します。

Apple Watch から iPhone への送信は、 リファレンスによると Apple Watch からの sendMessage で iPhone アプリが通信可能な状態になるとのことなので、 sendMessage を使用します。

実際の開発では、アプリの仕様によって適切な送信メソッドは変わってきます。

iPhoneの送受信処理です。 送信データ型は [String : Any] 型のDictionaryになります。
(transferUserInfo はシミュレータでは実行できませんのでご注意ください。)

また、 WCSessionDelegate の sendMessage 受信用メソッドを実装し、受け取った値でカウンター値を更新します。

final class PhoneContentViewModel: NSObject, ObservableObject {

    ...
    
    func countup() {
        counter += 1
        send(count: counter)
    }
    
    func countdown() {
        counter -= 1
        send(count: counter)
    }
    
    ///
    /// カウントを送信
    /// - Parameter count: カウンタの値
    /// - Warning: シミュレータでの実行は不可
    ///
    func send(count: Int) {
        guard session.activationState == .activated else {
            print("Sending method can only be called while the session is active.")
            return
        }

        Task.detached(priority: .medium) { [self] in
            let userInfo = [
                "count" : count
            ] as [String : Any]
            
            session.transferUserInfo(userInfo)
        }
    }
}

extension PhoneContentViewModel: WCSessionDelegate {
    
    ...
    
    func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
        Task { @MainActor in
            if let value = message["count"] as? Int {
                counter = value
            }
        }
    }
}

続いてApple Watchの送信処理です。似ていますね。
WCSessionDelegate の受信メソッドは transferUserInfo 用を実装します。

final class WatchContentViewModel: NSObject, ObservableObject {

    ...
    
    func countup() {
        counter += 1
        sendImmediately(count: counter)
    }
    
    func countdown() {
        counter -= 1
        sendImmediately(count: counter)
    }
    
    
    ///
    /// 直ちにカウントを送信
    /// - Parameter count: カウンタの値
    ///
    func sendImmediately(count: Int) {
        guard session.activationState == .activated else {
            print("Sending method can only be called while the session is active.")
            return
        }
        
        Task.detached(priority: .medium) { [self] in
            let message = [
                "count" : count
            ] as [String : Any]
            
            self.session.sendMessage(message, replyHandler: nil) { error in
                print(error)
            }
        }
    }
}

extension WatchContentViewModel: WCSessionDelegate {
    
    ...
    
    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        Task { @MainActor in
            if let value = userInfo["count"] as? Int {
                counter = value
            }
        }
    }
}

5. 動作確認

以上で実装は完了です。
実際に動かすと、以下の動画のように iPhone と Apple Watch でカウンターの値を連動できました。

(Apple Watch側の値が変わらない場合、一時的に通信不能になっている可能性があります。Apple Watch の画面をタップして再度試してみてください。)

まとめ

Watch Connectivity を使って iPhone と Apple Watch の双方向通信を実装しました。

説明の途中で「Apple Watch が一時的に通信不能になる」という話があったように、実際に Apple Watch アプリを開発すると少し動作にクセがありますが、今回の内容を応用すれば Apple Watch のセンサーを使った動作計測や、U1チップを使った位置測定なども可能になります。

n/a

iOS・Androidアプリを開発して10年になります。
最近はAWSを勉強しています。

関連記事