watchOS Tutorial: Kommunikation zwischen iPhone und Apple Watch

von @ralfebert · aktualisiert am 13. Juli 2021

Das folgende Tutorial richtet sich an iOS-Entwickler und führt in die Entwicklung für die Apple Watch ein. Es zeigt zudem die Verwendung des WatchKit Connectivity-Frameworks um Informationen zwischen Apple Watch und iPhone auszustauschen.

  1. Verwende für dieses Tutorial die aktuelle Version von Xcode 13. Dieses Tutorial wurde zuletzt getestet am 6. Juli 2021 mit Xcode 13 Beta 2.

  2. Erstelle mit File » New » Project » watchOS » Application » iOS App with Watch App ein neues iOS-App-Projekt mit Watch App:

    Create new iOS App with WatchKit App

    WatchKit Apps können als Teil einer iOS-App oder als eigenständige App ausgeliefert werden. Dieses Tutorial zeigt die Variante, bei dem die App vom iPhone als Teil einer iOS App auf der Apple Watch installiert wird und mit dieser kommunizieren kann.

  3. Gib als Projektname ConnectivityExample an. Achte darauf, dass als Interface-Technologie SwiftUI ausgewählt ist und wähle alle anderen Optionen ab, um das einfachstmögliche Projekt zu erstellen:

    Project Settings

Listendarstellung mit SwiftUI

  1. Erstelle in der WatchKit Extension eine Klasse PhoneMessaging, die die Kommunikation mit der iPhone-App verwaltet und die Antworten aufbewahrt. Implementiere sie via ObservableObject/@Published so, dass die Liste der Nachrichten beobachtet werden kann:

    class PhoneMessaging: NSObject, ObservableObject {
        @Published var messages = [String]()
    
        func requestInfo() {
            messages.append("msg")
        }
    }
    
  2. Öffne das ContentView in der WatchKit Extension und implementiere dies so, dass die Nachrichten angezeigt werden und die Möglichkeit besteht, eine Anfrage zu senden:

    struct ContentView: View {
        @StateObject var phoneMessaging = PhoneMessaging()
    
        var body: some View {
            ScrollView {
                VStack(alignment: .leading) {
                    Button("Request Info") {
                        phoneMessaging.requestInfo()
                    }
                    ForEach(phoneMessaging.messages, id: \.self) { message in
                        Text(message)
                    }
                }
            }
        }
    }
    
  3. Wähle einen der Apple Watch Simulatoren für das WatchKit-App-Target aus:

  4. Starte die App mit ⌘R testweise:

    Simulator Ready

Anfragen der Watch-App in der iPhone-App beantworten

Da die Apple Watch über weniger Rechenleistung und Hardwarefähigkeiten als das iPhone verfügt, müssen gelegentlich Aufgaben von der iPhone-App übernommen werden. Die Kommunikation erfolgt mit dem WatchConnectivity-Framework. Im ersten Schritt werden wir in der iPhone-App Anfragen der watchOS-App nach dem aktuellen Datum beantworten (dieses Beispiel dient lediglich zu Demonstrationszwecken):

  1. Deaktiviere in der WatchKit Extension die Option Supports Running Without iOS App Installation. Dies scheint notwendig zu sein, um Nachrichten zwischen der watchOS-App und der iOS-App auszutauschen:

  2. Erstelle in der iOS-App eine Klasse ConnectivityRequestHandler. Registriere diese bei der WCSession und implementiere das WCSessionDelegate-Protokoll mit Konsolenausgaben. Zur Fehleranalyse empfiehlt es sich, die Eigenschaften paired und watchAppInstalled auszugeben, wenn die Session aktiviert wurde.

    import WatchConnectivity
    
    class ConnectivityRequestHandler: NSObject, ObservableObject {
        var session = WCSession.default
    
        override init() {
            super.init()
            session.delegate = self
            session.activate()
        }
    }
    
    extension ConnectivityRequestHandler: WCSessionDelegate {
    
        func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
            debugPrint("WCSession activationDidCompleteWith activationState:\(activationState) error:\(String(describing: error))")
            debugPrint("WCSession.isPaired: \(session.isPaired), WCSession.isWatchAppInstalled: \(session.isWatchAppInstalled)")
        }
    
        func sessionDidBecomeInactive(_ session: WCSession) {
            debugPrint("sessionDidBecomeInactive: \(session)")
        }
    
        func sessionDidDeactivate(_ session: WCSession) {
            debugPrint("sessionDidDeactivate: \(session)")
        }
    
        func sessionWatchStateDidChange(_ session: WCSession) {
            debugPrint("sessionWatchStateDidChange: \(session)")
        }
    
    }
    
  3. Ergänze ConnectivityRequestHandler so, dass die Anfragen über den Aufruf des replyHandler-Blockes beantwortet werden und beantworte eine Anfrage nach dem aktuellen Datum:

    extension ConnectivityRequestHandler: WCSessionDelegate {
    
        // ...
    
        func session(_: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
        debugPrint("didReceiveMessage: \(message)")
        if message["request"] as? String == "date" {
            replyHandler(["date": Date()])
        }
    }
    }
    
  4. Erzeuge in der ConnectivityExampleApp einen ConnectivityRequestHandler und halte es als @StateObject:

    @main
    struct ConnectivityExampleApp: App {
        @StateObject var connectivityRequestHandler = ConnectivityRequestHandler()
    
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    
  5. Prüfe einmal via Window » Devices and Simulators, welcher iPhone-Simulator mit dem gewählten watchOS-Simulator verbunden ist und starte die App einmal in diesem Simulator:

  6. Prüfe über die Konsolenausgabe, ob die Simulatoren korrekt verbunden sind:

    "WCSession.isPaired: true, WCSession.isWatchAppInstalled: true"
    

Informationen aus der WatchOS-App von der iPhone-App anfordern

  1. Passe die Klasse PhoneMessaging in der WatchKit-Extension so an, dass hier ebenfalls eine WCSession aktiviert wird und eine Anfrage gesendet wird:

    class PhoneMessaging: NSObject, ObservableObject {
        @Published var messages = [String]()
    
        var session: WCSession?
    
    override init() {
        session = WCSession.default
        super.init()
        session?.delegate = self
        session?.activate()
    }
    
        func requestInfo() {
            let request = ["request": "date"]
    session?.sendMessage(
        request,
        replyHandler: { response in
            debugPrint("Received response", response)
            DispatchQueue.main.async {
                self.messages.append("Reply: \(response)")
            }
        },
        errorHandler: { error in
            debugPrint("Error sending message:", error)
        }
    )
        }
    }
    
    extension PhoneMessaging: WCSessionDelegate {
        func session(_: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
            debugPrint("activationDidCompleteWith activationState:\(activationState.rawValue), error: \(String(describing: error))")
        }
    }
    
  2. Starte die App im Watch-Simulator und teste, dass die Anfrage beantwortet wird (die iPhone-App wird dazu automatisch im Hintergrund gestartet - es kann einige Sekunden dauern, bis die Antwort vorliegt):

    WatchConnectivity Response
  3. Hinweis: Sollte die Kommunikation zwischen Watch und iOS App nicht funktionieren, prüfe die Watch App Installed-Ausgabe im iPhone-Simulator. Sollte diese false sein, öffne die Watch-App im Simulator und entferne manuell die Watch-App und installiere sie erneut, um die Apps korrekt zu verbinden.

Nachrichten vom iPhone an die Watch-App senden, Hintergrund-Nachrichten

Mit sendMessage können auch Nachrichten aus der iPhone-App an die Watch-App gesendet werden. Hierbei muss jedoch die App auf der Uhr bereits gestartet sein („reachable“ sein), um Nachrichten zu empfangen.

Mit den WCSession-Methoden updateApplicationContext und transferUserInfo können Nachrichten ausgetauscht werden, ohne dass die App auf dem anderen Gerät aktiv sein muss oder im Hintergrund aktiviert werden muss. Der Austausch erfolgt dabei auf Batterieverbrauch hin optimiert zu einem günstigen Zeitpunkt, z.B. zusammen mit Nachrichten anderer Apps.

updateApplicationContext überschreibt dabei bereits vorhandene ältere Werte mit neuen Werten, so dass nur der neueste Stand übertragen wird. Mit transferUserInfo erhält die andere App alle Nachrichten. Bei der Verwendung des ApplicationContext ist zu beachten, dass die Eigenschaft session.applicationContext nur die zuletzt gesendeten Werte enthält, empfangene Werte werden ausschließlich über die Delegate-Methode didReceiveApplicationContext zugestellt.

Weitere Informationen