3. August 2019

watchOS 4 Tutorial: Kommunikation zwischen iPhone und Apple Watch

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.

Verwende für dieses Tutorial die aktuelle Version von Xcode 10. Dieses Tutorial wurde zuletzt getestet am 3. August 2019 mit Xcode 10.3.

  1. WatchKit Apps werden als Teil einer iOS-App ausgeliefert und vom iPhone auf der Apple Watch installiert: Erstelle mit File » New » Project » watchOS » Application » iOS App with WatchKit App ein neues iOS-App-Projekt mit WatchKit App:

    Create new iOS App with WatchKit App:

  2. Gib als Projektname ConnectivityExample an. Achte darauf, dass als Programmiersprache Swift ausgewählt ist und wähle alle Optionen ab, um das einfachstmögliche Projekt zu erstellen:

    Project Settings

Listendarstellung mit WatchKit

  1. Öffne das Interface.storyboard der WatchKit-App und füge über die Object Library ein Table-Element mit einem Label sowie einen Button mit dem Text „Request Info“ hinzu:

    Add Button To Watchkit App
  2. Vergib für das RowController-Objekt einen Identifier MessageRow:

    Identifier für RowController
  3. Konfiguriere für das Label die Eigenschaft Lines auf 0, um mehrzeilige Texte zu unterstützen:

    Label: Eigenschaft Lines
  4. Öffne den Code der InterfaceController-Klasse im Assistant Editor und ziehe mit der rechten Maustaste ein Outlet messagesTable für die Tabelle und eine Action-Methode requestInfo für den Button:

    WatchKit InterfaceController: Outlets und Actions
  5. Erstelle eine MessageRow-Klasse und konfiguriere sie für den RowController als Custom Class. Ziehe eine Outlet-Verbindung label für das Label in der Tabellenzeile:

    Row-Controller Custom Class
  6. Implementiere den InterfaceController so, dass alle Nachrichten in einem Array abgelegt und in der Tabelle angezeigt werden. Beachte dabei, dass Benachrichtigungen auf einer Hintergrund-Queue erfolgen und die Oberflächenelemente nur vom Main Thread aus verwendet werden dürfen:

    class InterfaceController: WKInterfaceController {
    
        // ...
        
        // MARK: - Messages Table
        
        var messages = [String]() {
            didSet {
                OperationQueue.main.addOperation {
                    self.updateMessagesTable()
                }
            }
        }
    
        func updateMessagesTable() {
            messagesTable.setNumberOfRows(messages.count, withRowType: "MessageRow")
            for (i, msg) in messages.enumerated() {
                let row = messagesTable.rowController(at: i) as! MessageRow
                row.label.setText(msg)
            }
        }
        
    }
  7. Füge testweise in awakeWithContext eine Nachricht hinzu:

    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        
        messages.append("ready")
    }
  8. Wähle einen der iPhone + Apple Watch Simulatoren für das WatchKit-App-Target aus:

    Select Watch Simulator

  9. Starte die App mit ⌘R testweise:

    Simulator Ready

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

Da die Apple Watch über weniger Rechenleistung und Hardwarefähigkeiten als das iPhone verfügt, müssen häufig Aufgaben von der iPhone-App übernommen werden. Die Kommunikation erfolgt mit dem WatchConnectivity-Framework. Im ersten Schritt werden wir der iPhone-App eine Anfrage senden, die diese beantworten kann, selbst wenn sie gerade nicht aktiv ist.

  1. Importiere das WatchConnectivity-Framework. Erstelle in der willActivate-Methode ein WCSession-Objekt, konfiguriere den Controller als Delegate bei der Session und aktiviere die Session:

    import WatchKit
    import Foundation
    import WatchConnectivity
    
    class InterfaceController: WKInterfaceController, WCSessionDelegate {
    
        var session : WCSession?
        
        // ...
    
        override func willActivate() {
            // This method is called when watch view controller is about to be visible to user
            super.willActivate()
    
            session = WCSession.default
    session?.delegate = self
    session?.activate()
        }
    
        // ...
    
        // MARK: - WCSessionDelegate
    
        func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
            NSLog("%@", "activationDidCompleteWith activationState:\(activationState) error:\(error)")
        }
    }
  2. Rufe in der Button-Action-Methode die Methode sendMessage der WatchConnectivity-Session auf und übergebe einen replyHandler-Block. Damit wird der iPhone-App eine Nachricht gesendet und diese hat die Möglichkeit, mit Daten in einem Dictionary zu antworten. Zeige die Antwort über das messages-Array an:

    @IBAction func requestInfo() {
        session?.sendMessage(["request" : "date"],
            replyHandler: { (response) in
                self.messages.append("Reply: \(response)")
            },
            errorHandler: { (error) in
                print("Error sending message: %@", error)
            }
        )
    }
  3. Erstelle in der iOS-App (nicht in der WatchKit-Extension) eine Klasse ConnectivityHandler, um hier ebenfalls eine WCSession zu erzeugen und zu aktivieren. Zur Fehleranalyse empfiehlt es sich, die Eigenschaften paired und watchAppInstalled auszugeben. Implementiere die Klassen so, dass die Anfragen über den Aufruf des replyHandler-Blockes beantwortet werden:

    import Foundation
    import WatchConnectivity
    
    class ConnectivityHandler : NSObject, WCSessionDelegate {
    
        var session = WCSession.default
    
        override init() {
            super.init()
    
            session.delegate = self
            session.activate()
    
            debugPrint("%@", "Paired Watch: \(session.isPaired), Watch App Installed: \(session.isWatchAppInstalled)")
        }
    
        // MARK: - WCSessionDelegate
    
        func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
            debugPrint("activationDidCompleteWith activationState:\(activationState) error:\(error)")
        }
    
        func sessionDidBecomeInactive(_ session: WCSession) {
            debugPrint("sessionDidBecomeInactive: \(session)")
        }
    
        func sessionDidDeactivate(_ session: WCSession) {
            debugPrint("sessionDidDeactivate: \(session)")
        }
    
        func sessionWatchStateDidChange(_ session: WCSession) {
            debugPrint("sessionWatchStateDidChange: \(session)")
        }
    
        func session(_ 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 im AppDelegate einen ConnectivityHandler:

    import UIKit
    import WatchConnectivity
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        var window: UIWindow?
        var connectivityHandler : ConnectivityHandler?
    
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
            if WCSession.isSupported() {
        self.connectivityHandler = ConnectivityHandler()
    } else {
        debugPrint("WCSession not supported (f.e. on iPad).")
    }
    
            // Override point for customization after application launch.
            return true
        }
    
    }
  5. Starte die App mit ⌘R im Watch-Simulator und prüfe, dass die Anfrage beantwortet wird (die iPhone-App wird dazu automatisch im Hintergrund gestartet):

    WatchConnectivity Response

  6. Hinweis: Sollte die Kommunikation zwischen Watch und iOS App nicht funktionieren, prüfe die Watch App Installed-Ausgabe im iOS-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:

    Workaround: Watch App Not Installed

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

Mit sendMessage können auch Nachrichten aus der iPhone-App an die WatchKit-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.

Das Beispielproject zu diesem Tutorial enthält auch Beispielcode zur Verwendung dieser APIs.