Via Gattinella, 2 50013 Campi Bisenzio +39 055 8969730 staff@madeinapp.net

Swift Development: Introduzione Watch Connectivity

In questo tutorial Swift faremo una semplice app per scambiare dati fra iPhone e Apple Watch.

Durante questo tutorial impareremo:

  1. Come creare un app per Apple Watch
  2. Usare tutte le modalità  di WatchConnectivity per scambiarsi dati fra 2 app
  3. Fare una semplice ToDo List che invia dati da iPhone a Apple Watch

 

Prima di iniziare a scrivere codice iniziamo a spiegare quali sono i modi per scambiare dati da Apple Watch a iPhone e viceversa attraverso le sessioni di WatchConnectivity:

Come ricevere data in WatchOS 4:

  • Le sessioni sono dei singleton e ne abbiamo una per app quindi una su iPhone e una su Apple Watch
  • Inviare dati è immediato con applicationContext e userInfo quando l’app sul Watch è aperta
  • Non scordarti di attivare le sessioni da entrambi i lati altrimenti i dati non passeranno
  • Puoi accedere alla sessione e inviare i dati ovunque nella tua app, ma ci sarà solo un luogo responsabile della ricezione delle info dell’altro dispositivo (la sessione singleton)

Tipi di Comunicazione:

Ci sono due tipi di comunicazione, Background Transfers e Interactive Messaging che a sua volta hanno i relativi metodi:

Background Transfer: utilizzato quando le informazioni non sono necessarie immediatamente, l’OS determina il momento opportuno per inviare i dati e il contenuto viene messo in coda per il trasferimento.

  1. Application Context: Usato quando solo le ultime informazioni sono necessarie, poiché ogni chiamata sostituisce il dictionary della precedente chiamata. Quando updateApplicationContext viene chiamato da un lato, didReceiveApplicationContext verrà chiamato dall’altro. Una volta avviata l’app sul Watch, il contenuto viene consegnato e l’orologio può aggiornarne lo stato.
  2. User Info Transfer: Il contenuto viene messo in coda e consegnato secondo l’ordine FIFO (First in First out), a differenza dell’applicazion Context niente viene sovrascritto
  3. File Transfer: Con questa modalità di trasferimento è possibile inviare anche un file oltre ad un dictionary che può essere anche nil

Interactive Messaging: Richiede uno stato di sessione raggiungibile su entrambi i dispositivi e l’app dev’essere in foreground. Viene utilizzato quando le informazioni sono necessarie immediatamente e entrambe le app sono in esecuzione allo stesso tempo.

  1. Interactive Messaging: Con questa modalità abbiamo 2 metodi, uno per inviare dictionary e uno per inviare data

Note Importanti:

Tutti dicono di chiamare activateSession() il prima possibile. Ma cosa significa? Suggerisco di chiamare activateSession() nella init di AppDelegate (per iPhone) e nella init di ExtensionDelegate (per Apple Watch). Questo mantiene il codice coerente e organizzato.

Inoltre consiglio di usare una sharedInstance di WatchManager su iPhone in modo da poter chiamare le funzioni della sessioneWC in qualsiasi parte del codice.

Esempio di contesto applicativo

L’esempio che vediamo in questo tutorial è la semplificazione di un app che abbiamo sviluppato per Suity che inviava gli ordini effettuati da iPhone a Apple Watch per poterli vedere aggiornati su Apple Watch, nel nostro caso invece invieremo messaggi scritti dall’app e li metteremo in una table su Apple Watch. Nel nostro caso l‘Apple Watch doveva riattivare il telefono e ciò avveniva inviando un messaggio quando l’app sul Watch è aperta. Nel caso in cui l’iPhone associato sia raggiungibile, sendMessage dovrebbe avviare l’app per iPhone in background e sarà possibile aggiornare lo stato da lì, nel nostro esempio invece è necessario che entrambe le app siano aperte quindi non sarà necessario inviare un message per “svegliare” l’app su iPhone.

Adesso passiamo al codice con il WatchManager:

//
//  WatchManager.swift
//

import UIKit
import WatchConnectivity

class WatchManager: NSObject, WCSessionDelegate {
    
    static let sharedInstance = WatchManager()
    var session = WCSession.default
    
    
    override init() {
        super.init()
        
        session.delegate = self
        session.activate()
        
        print("%@", "Paired Watch: \(session.isPaired), Watch App Installed: \(session.isWatchAppInstalled)")
    }
    
    // MARK: - WCSessionDelegate
    
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        print("%@", "activationDidCompleteWith activationState:\(activationState) error:\(String(describing: error))")
    }
    
    func sessionDidBecomeInactive(_ session: WCSession) {
        print("%@", "sessionDidBecomeInactive: \(session)")
    }
    
    func sessionDidDeactivate(_ session: WCSession) {
        print("%@", "sessionDidDeactivate: \(session)")
    }
    
    func sessionWatchStateDidChange(_ session: WCSession) {
        print("%@", "sessionWatchStateDidChange: \(session)")
    }
    
    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        print("didReceiveMessage: %@", message)
        
    }
    
    func sendMessageByUserInfo(message : [String : String]){
        session.transferUserInfo(message as [String:Any])
    }
    
    func sendMessageByTransferFile(message : [String : String]){
        let filePath = NSURL.fileURL(withPath: Bundle.main.path(forResource: "images", ofType: "jpg")!)
        session.transferFile(filePath, metadata: message as [String:Any])
    }
    
    func sendMessageByUpdateApplicationContext(message : [String : String]){
            do {
                try session.updateApplicationContext(message as [String:Any])
            } catch let error {
                print("error update application context \(error)")
            }
    }
    
    func sendMessageBySendMessage(message : [String:String]){
        session.sendMessage(message as [String:Any], replyHandler: nil, errorHandler: nil)
    }
}

 e per far iniziare la sessione il prima possibile nel didFinishLaunchingWithOptions dell’AppDelegate inseriamo il seguente codice:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        if WCSession.isSupported() {
            self.connectivityHandler = WatchManager.sharedInstance
        } else {
            print("WCSession not supported (f.e. on iPad).")
        }
        
        return true
    }

Dopo aver “gestito” le sessioni lato iPhone creiamo una maschera per inviare messaggi al Watch simile a questa:

 

e dopo aver collegato tutti gli outlets e l’action del button al View Controller dobbiamo creare un dictionary [String:Any] da inviare al Watch, il dictionary che creeremo sarà il nostro Message e tramite i metodi del WatchManager saremo in grado di inviarlo al nostro Apple Watch

//
//  ViewController.swift
//

import UIKit
import WatchConnectivity

class ViewController: UIViewController {
    
    @IBOutlet weak var titleField: UITextField!
    @IBOutlet weak var descriptionField: UITextView!
    @IBOutlet weak var colorSelector: UISegmentedControl!
    
    @IBAction func pressSendMessage(_ sender: Any) {
        var message : [String : String] = [:]
        message["title"] = titleField.text
        message["description"] = descriptionField.text
        message["color"] = String(colorSelector.selectedSegmentIndex)
        switch colorSelector.selectedSegmentIndex {
        case 0:
            WatchManager.sharedInstance.sendMessageByUserInfo(message : message)
            break
        case 1:
            WatchManager.sharedInstance.sendMessageBySendMessage(message: message)
            break
        case 2:
            WatchManager.sharedInstance.sendMessageByUpdateApplicationContext(message: message)
            break
        default:
            break
        }
    }
}

A questo punto l’app su iPhone è pronta e ci dobbiamo concentrare su quella per Watch, iniziamo con il creare il nuovo target quindi andiamo in
File\New\Target…. Dopodiché scegliamo watchOS\Application\WatchKit App e andiamo avanti. Nella schermata successiva mettiamo il nome dell’app, assicuriamoci che il linguaggio sia Swift, deselezioniamo le 2 checkbox e infine clicchiamo su Finish.

Perfetto abbiamo creato l’app per Apple Watch, come possiamo vedere ci sono 2 cartelle, la prima è uguale al nome dell’app e la seconda è nomeapp + Extension. Nell’Extension ci vanno tutte le classi mentre nel nomeapp ci vanno tutti gli storyboard e gli assets. Adesso andiamo nell’Extension creiamo un model Message con (title , subtitle, color) e un RowController per gestire le singole Row che conterrano i messaggi.

 

//
//  Message.swift
//

import Foundation

class Message : NSObject{
    var title : String!
    var messageDescription : String!
    var color : Int!
    
    init(title : String, messageDescription : String, color : Int){
        self.title = title
        self.messageDescription = messageDescription
        self.color = color
    }
}


//
//  MessageRowController.swift
//

import WatchKit

class MessageRowController: NSObject {
    
    @IBOutlet var titleLabel: WKInterfaceLabel!
    @IBOutlet var descriptionLabel: WKInterfaceLabel!
    @IBOutlet var coloredSeparetor: WKInterfaceSeparator!
    
    var message: Message? {
        didSet {
            guard let message = message else { return }
            
            titleLabel.setText(message.title)
            descriptionLabel.setText(message.messageDescription)
            
            switch message.color {
            case 0:
                coloredSeparetor.setColor(UIColor.green)
                break
            case 1:
                coloredSeparetor.setColor(.yellow)
                break
            case 2:
                coloredSeparetor.setColor(.red)
                break
            default:
                coloredSeparetor.setColor(.white)
                break
            }
        }
    }
}

Nell’Interface.storyboard creaiamo un table simile a questa per ricevere tutti i messaggi, colleghiamo la Table all’IntefaceController mentre gli elementi dentro la Row vanno collegati al MessageRowController

 

Dopodichè non ci resta che andare nell’InterfaceController importare WatchConnectivity, attivare la sessione e aggiungere i delegati di sessionWC per ricevere i messaggi. Anche il reload della Table è fatto nell’InterfaceController perchè ogni volta che ci viene inviato un messaggio dobbiamo appenderlo nell’array di Message e ridisegnamo la Table, le single Row invece come abbiamo già visto sono gestite dal RowManager

//
//  InterfaceController.swift
//

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {
    
    @IBOutlet var messageTable: WKInterfaceTable!
    @IBOutlet var noMessageLabel: WKInterfaceLabel!
    
    var session  :  WCSession?
    var messages = [Message]() {
        didSet {
            OperationQueue.main.addOperation {
                self.updateOrderTable()
            }
        }
    }
    
    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        noMessageLabel.setHidden(true)
        updateOrderTable()
        self.setTitle("WKExample")
    }
    
    override func willActivate() {
        super.willActivate()
        session = WCSession.default
        session?.delegate = self
        session?.activate()
    }
    
    
    //MARK: Table
    func updateOrderTable() {
        if(messages.count > 0){
            noMessageLabel.setHidden(true)
            messageTable.setNumberOfRows(messages.count, withRowType: "MessageRowController")
            
            for index in 0..<messageTable.numberOfRows {
                guard let controller = messageTable.rowController(at: index) as? MessageRowController else { continue }
                controller.message = messages[index]
            }
            
        }else{
            noMessageLabel.setHidden(false)
        }
    }
    
    
    //MARK: Session
    func session(_ session: WCSession, didReceive file: WCSessionFile) {
        let messageReceived = file.metadata as! [String:String]
        messages.append(Message(title: messageReceived["title"]!, messageDescription: messageReceived["description"]!, color: Int(messageReceived["color"]!)!))
        updateOrderTable()
    }
    
    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        let messageReceived = userInfo as! [String:String]
        messages.append(Message(title: messageReceived["title"]!, messageDescription: messageReceived["description"]!, color: Int(messageReceived["color"]!)!))
        updateOrderTable()
    }
    
    func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
        let messageReceived = applicationContext as! [String:String]
        messages.append(Message(title: messageReceived["title"]!, messageDescription: messageReceived["description"]!, color: Int(messageReceived["color"]!)!))
        updateOrderTable()
    }
    
    func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
        let messageReceived = message as! [String:String]
        messages.append(Message(title: messageReceived["title"]!, messageDescription: messageReceived["description"]!, color: Int(messageReceived["color"]!)!))
        updateOrderTable()
    }
    
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        NSLog("%@", "activationDidCompleteWith activationState:\(activationState) error:\(String(describing: error))")
    }

}

Congratulazioni abbiamo finito le nostre app, non ci resta che buildare il progetto con il target del watch e provare questa ToDoList!

Happy Coding :]

 

Download Project: WatchConnectivityExample

leave a comment