Come posso scambiare dati tra iPhone e Apple Watch con Swift?

Tutto è possibile grazie al framework WatchConnectivity.

In realtà è veramente molto semplice e comodo, basteranno poche righe di codice per scambiare dati e, volendo, interi files fra i due device.

Per prima cosa creiamo un nuovo progetto per Apple Watch (anche se si può estendere un progetto già esistente) ed andiamo ad inserire il framework WatchConnectivity negli import dell’interface controller.

Passo successivo, estendiamo col delegate WCSessionDelegate la classe InterfaceController.swift (quella dell’orologio per intenderci) e dichiariamo una variabile relativa alla sessione che andremo ad implementare var session : WCSession!:

import WatchKit
import Foundation
// 1: importare WatchConnectivity
import WatchConnectivity

// 2: aggiungere il delegato WCSessionDelegate
class InterfaceController: WKInterfaceController, WCSessionDelegate {
    // 3: creare l'oggetto del WCSession
    var session : WCSession!

A questo punto possiamo spostarci all’interno del metodo willActivate() della nostra classe e verifichiamo che il dispositivo in uso supporti il WCSession, altrimenti inutile tentare una comunicazione fra i due devices.

Prima di iniziare con la comunicazione tra i devices va creata la Watch Connectivity Session:

// 4: istanziamo la session chiamando il metodo defaultSession()
// per fare cio' dobbiamo prima verificare che WCSession sia supportato dal dispositivo 
if (WCSession.isSupported()) {
     session = WCSession.defaultSession()
     session.delegate = self
     session.activateSession()
} else { print("WCSession non e' supportato dal dispositivo in uso") }

Essenzialmente esistono 4 diversi modi per inviare dati da/a iPhone-Apple Watch, quello che prendiamo in considerazione al momento è quello definito “interattivo”, in quanto prende in considerazione anche una risposta da parte del device ricevente.

Per funzionare necessita di una connessione stabile fra i 2 devices (reachable state), in quanto i dati vengono spediti e ricevuti immediatamente.

Questo si traduce nei seguenti requisiti:

  • Apple Watch: connesso via bluetooth al telefono e applicazione in esecuzione (foreground).
  • iPhone: connesso via bluetooth all’orologio.

Si può verificare la connessione fra i devices con la seguente riga di codice:

if (WCSession.defaultSession().reachable) {
    // siamo connessi, quindi possiamo inviare dati
}

Possiamo ora inserire un bottone all’interno del progetto ed associare alla sua @IBAction un invio dei dati verso il telefono:

// 5: proviamo ad inviare un dato (una stringa) al cellulare
      let messageToSend = ["Messaggio":"Ciao dall'orologio"]
        session.sendMessage(messageToSend, replyHandler: { replyMessage in
            // gestione ed invio del messaggio
            let value = replyMessage["Messaggio"] as? String
            // scriviamo gli output su due UILabel per verificare l'invio
            self.myLabelRitorno.setText(value)
            self.myLabel.setText("inviato...")
            }, errorHandler: {error in
                // qui invece gestiamo eventuali errori
                print(error)
        })

Come si vede, la procedura e’ piuttosto semplice:

  • Preparo il testo da inviare.
  • Invio il testo tramite session.sendMessage e gestisco l’invio tramite la sua colture.

Ultima cosa che manca all’appello è la ricezione dei messaggi in arrivo dal device che invia, scriviamo quindi la seguente funzione di ricezione:

// 6: il metodo che risponde agli invii dal telefono
    func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
        // gestisco i messaggi in entrata
        let value = message["Messaggio"] as? String
        // scriviamo gli output su due UILabel per verificare la ricezione
        dispatch_async(dispatch_get_main_queue()) {
            self.myLabel.setText(value)
            self.myLabelRitorno.setText("ricevuto...")
        }
        // e mando una risposta al telefono
        replyHandler(["Messaggio":"ricezionde lato Watch: ok"])
    }

Il messaggio in entrata e in uscita viene presentato come message: [String : AnyObject], in effetti si tratta di un dizionario di cui dobbiamo conoscere la chiave per poterne leggere il valore.

Nel nostro caso abbiamo messo come chiave la stringa “Messaggio” ed abbiamo passato come valore un’altra stringa, ma nessuno vieta (AnyObject) di passare qualsiasi altro tipo di dato applicabile.

Il WCSession Class Reference di Apple ci dice infatti che:

A dictionary of property list values that you want to send. You define the contents of the dictionary that your counterpart supports. This parameter must not be nil.

Sempre secondo il Reference di Apple:

Calling this method from your WatchKit extension while it is active and running wakes up the corresponding iOS app in the background and makes it reachable. Calling this method from your iOS app does not wake up the corresponding WatchKit extension. If you call this method and the counterpart is unreachable (or becomes unreachable before the message is delivered), the errorHandler block is executed with an appropriate error.

Quindi, attenzione a come lo utilizzate, mandare un dato alla app, sveglia la app dal background, ma non avviene il contrario!

Per quanto riguarda il lato telefono, la situazione e’ esattamente identica, stessi metodi e stesse procedure sia in invio che in ricezione.

WCSessionDemo

Per completezza vediamo anche il codice lato iPhone, ma, come si può notare, si tratta della stesso codice:

import UIKit
// 7: importiamo il WatchConnectivity
import WatchConnectivity

// 8: aggiungiamo il delegato WCSessionDelegate
class ViewController: UIViewController, WCSessionDelegate {

    @IBOutlet weak var myLabel: UILabel!
    @IBOutlet weak var myLabelRitorno: UILabel!
    @IBOutlet weak var bottoneInvia: UIButton!
    
    // 9: dichiariamo la session anche lato iPhone
    var session: WCSession!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        // 10: anche qui istanziamo la session chiamando il metodo defaultSession()
        if (WCSession.isSupported()) {
            session = WCSession.defaultSession()
            session.delegate = self;
            session.activateSession()
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func azioneInvia(sender: UIButton) {
        // 11: mandiamo un messaggio da iPhone al Watch
        let messageToSend = ["Messaggio":"Ciao dal telefono"]
        session.sendMessage(messageToSend, replyHandler: { replyMessage in
            //handle the reply
            let value = replyMessage["Messaggio"] as? String
            //use dispatch_asynch to present immediately on screen
            dispatch_async(dispatch_get_main_queue()) {
                self.myLabelRitorno.text = value
                self.myLabel.text = "inviato..."
            }
            }, errorHandler: { error in
                // catch any errors here
                print(error)
        })
        
    }
    
    // 12: il metodo che risponde agli invii dall'orologio
    func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
        // gestisco il messaggio in entrata
        let value = message["Messaggio"] as? String
        dispatch_async(dispatch_get_main_queue()) {
            self.myLabel.text = value
            self.myLabelRitorno.text = "ricevuto..."
        }
        // invio una risposta all'orologio
        replyHandler(["Messaggio":"ricezione lato iPhone: ok"])
    }

}

Con questo si chiude la programmazione della comunicazione “interattiva” fra i devices.

Background messaging

I 3 metodi rimanenti appartengono a questa categoria e non necessitano di connessione attiva fra i 2 devices in quanto:

  • I messaggi vengono accodati.
  • Il sistema operativo decide quando inviare i dati in base alle esigenze del device in uso.
  • Risulta una soluzione ottimale se non si necessita immediatamente di questi dati.

Il primo metodo che andremo a provare è quello dell’Application Context, sostituiamo nel codice precedente i punti 5 e 6:

    @IBAction func azioneInvia() {
        // 5: proviamo ad inviare un dato (una stringa) al cellulare
        let messageToSend = ["Messaggio":"orologio"]
        do
        {
            try session?.updateApplicationContext(messageToSend)
        }
        catch
        {
            print("error")
        }
    }
    
    // 6: il metodo che risponde agli invii dal telefono
    func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
        // gestisco i messaggi in entrata
        let value = applicationContext["Messaggio"] as? String
        //Use this to update the UI instantaneously (otherwise, takes a little while)
        dispatch_async(dispatch_get_main_queue())
        {
            if let value = value
            {
                self.myLabel.setText("\(value)")
            }
        }
    }

Il nuovo metodo, come i seguenti, non si aspetta una risposta da parte dell’altro device in virtù del non richiedere che il dispositivo sia raggiungibile in quell’esatto momento, ma, al contrario, aspetterà pazientemente che lo sia per inviargli i dati.

Molteplici invii di dati con questo metodo saranno inviati sequenzialmente, ma andranno persi a favore dell’ultimo spedito.

Ferma tutto!!! Mi stai dicendo che  se mando 4 messaggi arriva solo il quarto?

Ebbene si, in realtà è un ottimo sistema se consideriamo il caso di una app di news o di visualizzazione dei parametri vitali ottenuti dai sensori dell’orologio in tempo reale.

Dovessimo invece memorizzare tutti i parametri di uno sportivo durante una sessione di allenamento, allora tornerebbe utile il prossimo metodo: User Info Transfer.

        // 5: proviamo ad inviare un dato (una stringa) al cellulare
        let messageToSend = ["Messaggio":"orologio"]
        let transfer = WCSession.defaultSession().transferUserInfo(messageToSend)
        print(transfer)
    }
    
    // 6: il metodo che risponde agli invii dal telefono
    func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
        self.myLabel.setText(userInfo["Messaggio"] as! String)
    }

Il vantaggio di questo metodo è che i dati inviati non vengono persi, ma verranno consegnati secondo un ordine FIFO (First In First Out), ovvero il primo che entra è il primo ad uscire, in parole povere: in sequenza ordinata dal primo all’ultimo.

Per quanto riguarda l’ultimo metodo, si tratta Transfer File e serve proprio per trasferire i files da un dispositivo all’altro.

Ad onor del vero, questo metodo non l’ho provato personalmente ancora, non disponendo di un Apple Watch reale, ma mi fido della fonte:

https://developer.apple.com/library/prerelease/mac/samplecode/Lister/Listings/Swift_ListerKit__watchOS__ConnectivityListsController_swift.html

let url = // URL of file
let data = // Create dictionary of data 
let fileTransfer = WCSession.defaultSession().transferFile(url,
metadata:data)

// che viene ricevuto file da didReceiveFile

public func session(session: WCSession, didReceiveFile file: WCSessionFile) {
        copyURLToDocumentsDirectory(file.fileURL)
    }
    
    public func session(session: WCSession, didFinishFileTransfer fileTransfer: WCSessionFileTransfer, error: NSError?) {
        if error != nil {
            print("\(__FUNCTION__), file: \(fileTransfer.file.fileURL), error: \(error!.localizedDescription)")
        }
    }
    
    // MARK: Convenience
    
    private func copyURLToDocumentsDirectory(URL: NSURL) {
        let documentsURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
        let toURL = documentsURL.URLByAppendingPathComponent(URL.lastPathComponent!)
        
        ListUtilities.copyFromURL(URL, toURL: toURL)
    }

Ultima nota su questo metodo: Copiate il file prima dell’invio se volete mantenerne una copia, Apple spiega nel reference di:

optional func session(_ sessionWCSession,
didReceiveFile fileWCSessionFile)

File: The object containing the URL of the file and any additional information. If you want to keep the file referenced by this parameter, you must move it synchronously to a new location during your implementation of this method. If you do not move the file, the system deletes it after this method returns.

E con questo e’ tutto sul WCSession.

Buona programmazione :P