watchOS 3 Tutorial: Kommunikation zwischen iPhone und Apple Watch

Das folgende Tutorial richtet sich an iOS-Entwickler und zeigt die Verwendung des WatchKit Connectivity-Frameworks um Informationen zwischen Apple Watch und iPhone auszustauschen. Anhand eines Beispielprojektes wird Folgendes gezeigt:

Verwenden Sie für dieses Tutorial die aktuelle Version von Xcode 8. Dieses Tutorial wurde zuletzt getestet am 11.2.2017 mit Xcode 8.2.

  1. WatchKit Apps werden immer als Teil einer iOS-App erstellt und vom iPhone auf der Apple Watch installiert: Erstellen Sie 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. Geben Sie als Projektname ConnectivityExample an. Achten Sie darauf, dass als Programmiersprache Swift ausgewählt ist und wählen Sie alle Optionen ab, um das einfachstmögliche Projekt zu erstellen:

    Project Settings

Nachrichtenanzeige mit WKTable

  1. Öffnen Sie das Interface.storyboard der WatchKit-App und fügen Sie eine Table-Element mit einem Label sowie einen Button mit dem Text „Request Info“ hinzu:

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

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

    Label: Eigenschaft Lines
  4. Öffnen Sie den Code der InterfaceController-Klasse im Assistant Editor und ziehen Sie 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. Erstellen Sie eine MessageRow-Klasse und konfigurieren Sie sie für den RowController als Custom Class. Ziehen Sie eine Outlet-Verbindung label für das Label in der Tabellenzeile:

    Row-Controller Custom Class
  6. Implementieren Sie den InterfaceController so, dass alle Nachrichten in einem Array abgelegt und in der Tabelle angezeigt werden. Beachten Sie 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ügen Sie testweise in awakeWithContext eine Nachricht hinzu:

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

    Select Watch Simulator

  9. Starten Sie die App mit Cmd + R testweise:

    Simulator Ready

WatchConnectivity-Session aktivieren und Informationen 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. Da Apps seit Watch OS 2 auf der Uhr selbst ausgeführt werden muss dazu mit der App auf dem iPhone kommuniziert werden. Dies erfolgt mit dem Watch-Connectivity-Framework. Im ersten Schritt werden wir der iPhone-App Anfragen senden, die diese beantworten kann, selbst wenn sie gerade nicht aktiv ist.

  1. Importieren Sie das WatchConnectivity-Framework. Erstellen Sie in der willActivate-Methode ein WCSession-Objekt, konfigurieren Sie den Controller als Delegate bei der Session und aktivieren Sie 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. Rufen Sie in der Button-Action-Methode die Methode sendMessage der WatchConnectivity-Session auf und übergeben Sie einen replyHandler-Block. Damit wird der iPhone-App eine Nachricht gesendet und diese hat die Möglichkeit, mit Daten in einem Dictionary zu antworten. Zeigen Sie 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)
            }
        )
    }

Anfragen in iOS-App beantworten

  1. Erstellen Sie 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. Implementieren Sie 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()
    
            NSLog("%@", "Paired Watch: \(session.isPaired), Watch App Installed: \(session.isWatchAppInstalled)")
        }
    
        // MARK: - WCSessionDelegate
    
        func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
            NSLog("%@", "activationDidCompleteWith activationState:\(activationState) error:\(error)")
        }
    
        func sessionDidBecomeInactive(_ session: WCSession) {
            NSLog("%@", "sessionDidBecomeInactive: \(session)")
        }
    
        func sessionDidDeactivate(_ session: WCSession) {
            NSLog("%@", "sessionDidDeactivate: \(session)")
        }
    
        func sessionWatchStateDidChange(_ session: WCSession) {
            NSLog("%@", "sessionWatchStateDidChange: \(session)")
        }
    
        func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
            NSLog("didReceiveMessage: %@", message)
            if message["request"] as? String == "date" {
                replyHandler(["date" : "\(Date())"])
            }
        }
        
    }
  2. Erzeugen Sie 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 {
        NSLog("WCSession not supported (f.e. on iPad).")
    }
    
            // Override point for customization after application launch.
            return true
        }
    
    }

Watch App und iOS App parallel im Simulator starten

  1. Starten Sie die App mit Cmd + R im Watch-Simulator und prüfen Sie, dass die Anfrage beantwortet wird (die iPhone-App wird dazu automatisch im Hintergrund gestartet):

    WatchConnectivity Response

  2. Wählen Sie den zugehörigen iPhone-Simulator für das iOS-App-Target aus und starten Sie die App:

    Select iOS Simulator

    Es wird immer sowohl die Watch-App als auch die iOS-App gebaut und in den jeweiligen Simulatoren gestartet. Daher wird beim Start der Watch-App ein ggf. noch laufender iOS-Anwendungsprozess beendet, ebenso wird beim Start der iOS-App ein bereits laufender Watch-App-Prozess beendet.

  3. Starten Sie manuell im Watch-Simulator die Watch-App:

  4. Verwenden Sie Debug > Attach to Process > WatchKit Extension, um den Debugger an den soeben manuell gestarteten Watch-App-Prozess anzuhängen, ohne die parallel laufende iOS-App zu beenden:

  5. Hinweis: Sollte die Kommunikation zwischen Watch und iOS App nicht funktionieren, prüfen Sie die Watch App Installed-Ausgabe im iOS-Simulator. Sollte diese false sein, öffnen Sie die Watch-App im Simulator und entfernen Sie manuell die Watch-App und installieren Sie sie erneut, um die Apps korrekt zu verbinden:

    Workaround: Watch App Not Installed

Nachrichten mit sendMessage im Vordergrund austauschen

Mit sendMessage können Nachrichten zwischen Apple Watch und iOS/iPhone gesendet werden. Sendet die Uhr eine Nachricht an das Telefon, wird die App auf dem Telefon im Hintergrund aktiviert - in der umgekehrten Richtung muss die App auf der Uhr bereits gestartet sein („reachable“ sein), um Nachrichten zu empfangen.

  1. Fügen Sie dem ViewController im Storyboard der iOS-App einen Button Send Message und ein Label für die Nachrichtenanzeige hinzu und erstellen Sie eine Action-Methode für den Button und ein Outlet für das Label:

    ViewController UI
  2. Verwenden Sie den ConnectivityHandler der App, um an das WCSession-Objekt zu gelangen und eine Nachricht zu senden:

    class ViewController: UIViewController {
    
        @IBOutlet weak var messages: UILabel!
    
        var connectivityHandler : ConnectivityHandler!
    var counter = 0
        
        @IBAction func sendMessage(_ sender: Any) {
            counter += 1
    connectivityHandler.session.sendMessage(["msg" : "Message \(counter)"], replyHandler: nil) { (error) in
        NSLog("%@", "Error sending message: \(error)")
    }
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.connectivityHandler = (UIApplication.shared.delegate as? AppDelegate)?.connectivityHandler
        }
    
    }
  3. Implementieren Sie in der WatchKit Extension für die InterfaceController-Klasse die WCSessionDelegate-Methode session:didReceiveMessage: so dass die erhaltenen Nachrichten angezeigt werden:

    class InterfaceController: WKInterfaceController, WCSessionDelegate {
    
        // ...
    
        // MARK: - WCSessionDelegate
        
        func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
        let msg = message["msg"]!
        self.messages.append("Message \(msg)")
    }
    
    }
  4. Starten Sie die App testweise und testen Sie die Übermittlung der Nachrichten von der iPhone-App zur Watch. Starten Sie dazu die iOS-App per Product > Run und die Watch-App manuell über den Home-Screen:

    Message To Watch

Nachrichten von der Apple Watch zum iPhone senden

  1. Fügen Sie der ConnectivityHandler-Klasse eine dynamic-Eigenschaft messages hinzu, die mit einem Key-Value-Observer beobachtet werden kann und fügen Sie in die messages-Liste alle erhaltenen Nachrichten hinzu:

    class ConnectivityHandler : NSObject, WCSessionDelegate {
        
        // ...
        
        dynamic var messages = [String]()
               
        // MARK: - WCSessionDelegate
        
        func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
            // ...
        }
        
        func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
        let msg = message["msg"]!
        self.messages.append("Message \(msg)")
    }
    
    }
  2. Implementieren Sie den ViewController so, dass die messages-Liste beobachtet wird und die Oberfläche entsprechend aktualisiert wird:

    class ViewController: UIViewController {
    
        @IBOutlet weak var messages: UILabel!
    
        var connectivityHandler : ConnectivityHandler!
        var counter = 0
        
        @IBAction func sendMessage(_ sender: Any) {
            counter += 1
            connectivityHandler.session.sendMessage(["msg" : "Message \(counter)"], replyHandler: nil) { (error) in
                NSLog("%@", "Error sending message: \(error)")
            }
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.connectivityHandler = (UIApplication.shared.delegate as? AppDelegate)?.connectivityHandler
            self.connectivityHandler?.addObserver(self, forKeyPath: "messages", options: [], context: nil)
        }
    
        override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "messages" {
            OperationQueue.main.addOperation {
                self.updateMessages()
            }
        }
    }
    
    func updateMessages() {
        self.messages.text = self.connectivityHandler.messages.joined(separator: "\n")
    }
    
    deinit {
        self.connectivityHandler?.removeObserver(self, forKeyPath: "messages")
    }
    
    }
  3. Fügen Sie im Interface Storyboard der WatchKit App einen Button Send Message hinzu und implementieren Sie die Logik zum Senden von Nachrichten hier analog:

    class InterfaceController: WKInterfaceController, WCSessionDelegate {
    
        // ...
        
        var counter = 0
        
        @IBAction func sendMessage() {
            counter += 1
            session?.sendMessage(["msg" : "Message \(counter)"], replyHandler: nil) { (error) in
                NSLog("%@", "Error sending message: \(error)")
            }
        }
    
    }
  4. Lösen Sie eine Vibration beim Empfang einer neuen Nachricht aus:

    Watch OS:

    WKInterfaceDevice.current().play(.notification)

    iOS:

    import AudioToolbox
    // ...
    AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
  5. Starten Sie sowohl die Watch-App als auch die iOS-App im Simulator und testen Sie den Austausch von Nachrichten zwischen iOS und Watch OS:

    Nachrichten zwischen Apple Watch und iPhone

Nachrichten mit updateApplicationContext / transferUserInfo im Hintergrund austauschen

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.

  1. Fügen Sie im Storyboard von iOS und Watch-App neue Buttons „Update App Context“ und „Transfer User Info“ hinzu und verknüpfen Sie diese mit Action-Methoden und implementieren Sie sie so, dass die entsprechenden Methoden bei der WCSession aufgerufen werden:

    @IBAction func updateAppContext() {
        counter += 1
        try! connectivityHandler.session.updateApplicationContext(["msg" : "Message \(counter)"])
    }
    
    @IBAction func transferUserInfo() {
        counter += 1
        connectivityHandler.session.transferUserInfo(["msg" : "Message \(counter)"])
    }
  2. Ergänzen Sie in beiden Apps die entsprechenden Delegate-Methoden um Nachrichten der Gegenseite anzuzeigen:

    // MARK: - WCSessionDelegate
    
    func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
        let msg = applicationContext["msg"]!
        self.messages.append("AppContext \(msg)")
    }
    
    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        let msg = userInfo["msg"]!
        self.messages.append("UserInfo \(msg)")
    }
  3. Starten Sie die App im Simulator und testen Sie den Austausch von Nachrichten im Hintergrund:

    updateApplicationContext vs. transferUserInfo

Weitere Informationen

  1. Beispielproject ConnectivityExample
  2. App Programming Guide for watchOS: Sharing Data
  3. WWDC 2015: Introducing Watch Connectivity
Ralf Ebert Ralf Ebert ist langjähriger Entwickler für iOS und OS X und vermittelt in seinem Xcode-Buch seine Kenntnisse und Erfahrungswerte aus 5 Jahren Tätigkeit als Entwickler für OS X und iOS. Er hat mehrere Apps für Mac, iPhone und iPad konzipiert und entwickelt und vertreibt seine Apps erfolgreich über den App Store. Seit 2009 gibt er sein Know-How in iOS-Kursen für Softwareentwickler weiter, die von den Teilnehmern stets ausgezeichnet bewertet werden.