在 Swift 中使用 Watch Connectivity — Application Context

SwiftGG翻譯組發表於2019-01-26

原文連結=swift.gg/2018/08/15/…
作者:codingexplorer
譯者:Khala-wan
校對:Yousanflicswongzigii
定稿:CMB

在 Swift 中使用 Watch Connectivity — Application Context

在 watchOS 1 時代,WatchKit Extension 位於已配對的 iOS 裝置上,這使得宿主 APP 和 watch 之間的資料共享變得簡單。類似偏好設定這種最簡單的資料,只需要通過 App Groups 功能來存取 NSUserDefaults。目前在手機上留存的其他擴充套件程式和主 app 之間共享資料仍然應該使用這種方式,例如 Today View Extension,但它已不再適用於 watchOS 的 app。 幸運的是,蘋果為我們提供了新的 API 來做這件事。相比 App Groups,Watch Connectivity 擁有更強大的功能。它不僅提供了你的 Apple Watch 和與其配對 iPhone 之間連線狀態的更多資訊,還允許它們之間進行互動訊息和 3 種方式的後臺傳輸,這些方式分別是:

  1. Application Context
  2. User Info Transfer
  3. File Transfer

我們今天先討論第一種方式:Application Context。

什麼是 Application Context

假設你有個 watch app,它有一些可以在 iOS app 端設定的設定項,比如溫度的顯示單位是攝氏度還是華氏度。對於這樣的設定項,除非你希望在使用者在設定完成之後立即使用 watch 上的 app,否則將設定項的資訊通過後臺傳輸傳送到 watch 才會是比較合理的。

因為它可能不是立即需要的,所以系統可能會在節省電量最多的情況下將其傳送出去。你也不需要任何歷史記錄,因為使用者可能並不關心一小時之前的設定是攝氏度。

這就是 Application Context 的用武之地。它僅用於傳送最新的資料。如果你將溫度設定項從攝氏度改為華氏度,然後在 Application Context 傳送到 watch 之前再將它(或者其他設定項)設定為不同的值,那麼最新的值會覆蓋之前等待傳送的資訊。

如果你確實希望它能儲存先前資訊的歷史記錄,而且是以最省電的方式傳輸。那麼可以使用 User Info 方式進行傳輸。它的使用方式和 Application Context 很相似,但它會將更新操作加入到一個佇列中並逐一傳送(而不是僅僅覆蓋某些內容只傳送最新的資訊)。具體 User Info 的使用將作為以後另一篇文章的主題來講。

設定 iOS 應用程式

我們將從一個類似於上一篇文章 watchOS Hello World App in Swift 中的 app 說起。不過在本文中,我們將在這個 iPhone app 上加入一個 UISwitch 控制元件,並通過更新 watchOS app 上的 WKInterfaceLabel 來說明 UISwitch 的狀態。

首先,在 iOS app 的 viewController 中,我們需要設定一些東西:

import WatchConnectivity
 
class ViewController: UIViewController, WCSessionDelegate {
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { }
    func sessionDidBecomeInactive(_ session: WCSession) { }
    func sessionDidDeactivate(_ session: WCSession) { }
    
    var session: WCSession?
 
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if WCSession.isSupported() {
            session = WCSession.default
            session?.delegate = self
            session?.activate()
        }
    }
}

複製程式碼

下面,我們最先需要匯入 WatchConnectivity 框架。沒有它,我們所做的都是無用功。接下來,為了響應來自 WCSession 的回撥,我們需要將當前這個 ViewController 設定為 WCSession 的代理,為此我們需要讓它遵守這個協議,所以在 ViewController 的父類宣告後面新增 WCSessionDelegate 協議。

下一步,我們需要實現 WCSessionDelegate 中的一些方法。對於當前這個 app,它們不是特別必要,但是如果想要快速在 watch app 中切換,你就需要進一步實現它們。

之後,我們需要建立一個變數用於儲存 WCSession。因為 WCSession 實際上是一個單例,技術上我們可以跳過這一步,但每次輸入 session? 肯定要比 WCSession.default 更簡短。

你應該在程式碼執行初期對 session 進行設定。在大多數情況下,這將在程式初始化的時候來做。但由於我們是在 ViewController 中執行此操作,所以最早能執行的地方大概就只有 viewDidLoad 方法中了。一般情況下來說,你不應該在 viewController 中執行這個操作,因為你的 app 希望在螢幕上未載入特定 viewController 時就可以更新它的資料模型。為了簡單起見,我在 viewController 中做了這個操作,這僅僅是為了展示如何使用這些 API。如果這個 ViewController 是唯一關心使用 WCSession 的東西,那就沒關係。但通常情況並非如此。

要設定 session,我們需要先根據 WCSessionisSupport 方法的返回值來檢查是否支援。如果程式在 iPad 上執行的話,這一點尤為重要。目前,你無法將 iPad 與 Apple Watch 配對,因此它會返回 false 表示不支援在 iPad 上使用 WCSession。在 iPhone 上它會返回 true

一旦我們完成檢查,就可以將 WCSession 的 defaultSession 儲存在那裡,接著將這個 ViewController 設定為它的代理並啟用 session。如果我們可以在初始化程式中執行 isSupported 來測試是否支援,就可以把 session 用作一個常量。而這裡的 session 是一個可選值是因為我們不知道程式是否會執行在 iPad 上,所以當支援 WCSession 時,session 的值為 WCSession.defualt,反之則為 nil。這樣,當我們在 iPad 上嘗試訪問 session 中的屬性或方法時,它們甚至不會執行,因為 session 為 nil

將一個 UISwitch 放在 Storyboard 上,並將其 ValueChanged 方法關聯到 ViewController 中。 在方法中加入如下程式碼:

@IBAction func switchValueChanged(_ sender: UISwitch) {
    if let validSession = session {
        let iPhoneAppContext = ["switchStatus": sender.isOn]
 
        do {
            try validSession.updateApplicationContext(iPhoneAppContext)
        } catch {
            print("Something went wrong")
        }
    }
}

複製程式碼

首先檢查我們是否有一個有效的 session,如果是執行在 iPad 上,那麼將跳過整個程式碼塊。 Application Context 是一個 Swift 字典,它以 String 作為 keyAnyObject 作為 value (Dictionary<String, AnyObject>)。 value 必須遵循屬性列表的規則,並且只包含某些型別。它和 NSUserDefaults 具有相同的限制,所以在上一篇文章 NSUserDefaults — A Swift Introduction 中已經介紹過了具體可以使用哪些型別。儘管如此,當我們傳送一個 Swift Bool 型別時,其將會被轉換為 NSNumber boolean value,所以沒關係。

呼叫 updateApplicationContext 可能會丟擲異常,所以我們需要將它包裝在 do-block 中並通過 try 來呼叫。如果出現異常,我們只是在控制檯上列印了一些資訊,你還可以設定任何你需要的東西,比如你可能需要讓使用者知道發生了錯誤,那就可以顯示一個 UIAlerController,同樣,如果有必要可以加入異常的清理或恢復程式碼。這就是為了傳送 Application Context,我們所需要的全部準備。

設定 watchOS 應用程式

因為我們使用的是之前 watchOS Hello World App in Swift 文中的 Hello World App,所以部分相同的設定已經替我們完成了。跟 iPhone 類似,我們還需要做一些設定才能使用 WatchConnectivity

import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { }

    let session = WCSession.default

    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        
        session.delegate = self
        session.activate()
    }
//...
}

複製程式碼

這裡省略掉了之前 App 中的一些無關程式碼,只展示與 WatchConnectivity 設定相關的部分。同樣,我們需要匯入 WatchConnectivity 框架,並讓我們的 InterfaceController 遵守 WCSessionDelegate 協議,緊接著,我們將 session 常量初始化為 WCSession 的單例 defaultSession

與 iOS 端不同的是,這裡我們將 session 宣告為一個非可選值的常量。很顯然,執行在不低於 watchOS 2 系統上的 Apple Watch 是支援 Watch Connectivity 的,所以我們不需要在 watchOS 端進行相同的測試。 並且我們在宣告時就初始化了它,又沒有其他平臺(如iPad)需要擔心,所以我們不需要它是可選的。

接下來,在程式碼的初期,我們需要設定 session。在 InterfaceController 中 awakeWithContext 方法是個很好的地方,所以我們在這裡做相關設定。和 iOS App 一樣,我們設定當前類作為 session 的代理,然後啟用 session。

讓我們寫一個輔助方法來處理 Application Context 的回撥,因為我們可能會多次呼叫它,而不是僅僅當我們收到一個新 context 時(你很快會看到)。

func processApplicationContext() {
    if let iPhoneContext = session.receivedApplicationContext as? [String : Bool] {

        if iPhoneContext["switchStatus"] == true {
            displayLabel.setText("Switch On")
        } else {
            displayLabel.setText("Switch Off")
        }
    }
}

複製程式碼

WCSession 有 2 個與 Application Context 相關的屬性,applicationContextreceivedApplicationContext。它們的不同之處是:

  • applicationContext - 此裝置最近一次傳送Application Context
  • receivedApplicationContext - 此裝置最近一次接收Application Context

現在,把它倆放到一起來看,至少接收到的看起來很明顯。但在我第一次涉及這個時(不記得 WWDC 中 Watch Connectivity的介紹視訊的全部內容?),我認為 applicationContext 是從最近的傳送或接收來更新的,因為我認為它們是一致的 context。然而我大錯特錯,我花了一段時間才意識到它們是分開的。我當然能看出來原因,因為我們可能每次都會傳送不一樣的資料,就像從 Watch 的角度來看,applicationContext 就是 iPhone 端需要的 Watch 相關 context,而 receivedApplicationContext 則是 Watch 端需要的 iPhone 相關 context。無論哪種方式,請記住它們是不同的兩個東西,並根據實際情況選擇你所需要的那個。

所以在這個方法中,我們首先嚐試將 receivedApplicationContext[String: AnyObject] 型別的字典轉換為我們需要的 [String: Bool] 型別。如果轉換成功,則再根據字典中布林值的狀態將 displayLabel 的 text 值設定為 “Switch On” 或 “Switch Off”。

當我們實際接收到一個新的 Application context 時,該 InterfaceController 將會收到我們 WCSession 物件的代理回撥來通知我們這個資訊,我們將在那裡呼叫這個輔助方法。

func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
    DispatchQueue.main.async() {
        self.processApplicationContext()
    }
}
複製程式碼

現在,你大概看到了 didReceiveApplicationContext 方法的入參帶有它接收到的 Application Context 副本。它儲存在上面提到的 receivedApplicationContext 屬性中。所以我們並不需要它來呼叫輔助方法, 因此這個方法不需要傳入任何行參。

譯者注: 其實對於輔助方法 processApplicationContext 來說,增加行參 context 反而更 函式式,也更 Swift。 通過增加一個 context 的入參,可以讓方法內部實現和外部依賴解耦,更加方便我們對它進行單元測試。

那麼,呼叫 dispatch_async 是為了做什麼呢?好吧,這些代理回撥不在主執行緒上。你永遠不應該在除主執行緒以外的任何執行緒更新 iOS 或 watchOS 中的 UI。而我們的輔助方法除了從 receivedApplicationContext 中讀取資訊之外,主要目的是用來更新 UI 元素。因此,我們要通過 dispatch_async 方法返回主執行緒來呼叫該方法。呼叫 dispatch_async 需要 2 個引數,首先是派發佇列(對於主執行緒,我們通過 dispatch_get_main_queue 方法獲取),其次是一個閉包來告訴它需要做什麼操作,這裡我們只是告訴它去呼叫輔助方法。

所以,為什麼我們要在輔助方法裡這樣做,而不是直接在回撥方法裡面直接處理呢?好吧,當你實際接收到一個新的 Application Context 時,會回撥 didReceiveApplicationContext 代理方法。當 WCSession 在關閉時接收到新的 ApplicationContext 會呼叫 activateSession 方法,在那不久之後也會回撥到 didReceiveApplicationContext 方法。在這種情況下,我使用此 ApplicationContext 作為該資訊的後備儲存。我不確定這是不是一個好的主意,但是對於一個簡單的 app 來說,這是合理的, 因為 label 的重點是顯示 iPhone 上的 UISwitch 是開啟還是關閉。

那麼,當我們的 app 完成載入之後想使用最後一次接收到的值,但是 app 在關閉期間又沒有收到新的 context,這種情況該怎麼辦?我們在檢視生命週期的早期設定 label,所以現在 awakeWithContext 看起來應該是這樣:

override func awake(withContext context: Any?) {
    super.awake(withContext: context)
 
    processApplicationContext()
 
    session.delegate = self
    session.activate()
}

複製程式碼

由於 awakeWithContext 肯定在主執行緒上,我們不需要 dispatch_async。 因此這就是它僅用於在 didReceiveApplicationContext 回撥中來呼叫輔助方法而不是在輔助方法內部使用的原因。

此時 iOS App 並沒有保留該 UISwitch 的狀態,所以在啟動時保持它們的同步並不那麼重要,對於一個有價值的 app 來說,我們應該將 UISwitch 的狀態儲存在某個地方。比如可以在 iPhone 端使用 WCSession 的 ApplicationContext 屬性。(請記住,applicationContext 是從裝置傳送過來的最後一個 context),但如果是在iPad上執行呢?你可以將它儲存在 NSUserDefaults,或者其他許多地方,但這些不在如何使用 WatchConnectivity 的討論範疇內。具體你可以在早期的 NSUserDefaults — A Swift Introduction 文章中瞭解到。

程式碼

以下是該專案的完整程式碼:

ViewController.swift

import UIKit
import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate {
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { }
    func sessionDidBecomeInactive(_ session: WCSession) { }
    func sessionDidDeactivate(_ session: WCSession) { }
    
    @IBOutlet var theSwitch: UISwitch!
    
    var session: WCSession?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        if WCSession.isSupported() {
            session = WCSession.default
            session?.delegate = self
            session?.activate()
        }
    }
    
    func processApplicationContext() {
        if let iPhoneContext = session?.applicationContext as? [String : Bool] {
            if iPhoneContext["switchStatus"] == true {
                theSwitch.isOn = true
            } else {
                theSwitch.isOn = false
            }
        }
    }
    
    @IBAction func switchValueChanged(_ sender: UISwitch) {
        if let validSession = session {
            let iPhoneAppContext = ["switchStatus": sender.isOn]

            do {
                try validSession.updateApplicationContext(iPhoneAppContext)
            } catch {
                print("Something went wrong")
            }
        }
    }
}
複製程式碼

InterfaceController.swift

import WatchKit
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { }

    
    @IBOutlet var displayLabel: WKInterfaceLabel!
    
    let session = WCSession.default

    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        
        processApplicationContext()
        
        session.delegate = self
        session.activate()
    }
    
    @IBAction func buttonTapped() {
        //displayLabel.setText("Hello World!")
    }
    
    func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
        DispatchQueue.main.async() {
            self.processApplicationContext()
        }
    }
    
    func processApplicationContext() {
        if let iPhoneContext = session.receivedApplicationContext as? [String : Bool] {
            
            if iPhoneContext["switchStatus"] == true {
                displayLabel.setText("Switch On")
            } else {
                displayLabel.setText("Switch Off")
            }
        }
    }
}
複製程式碼

請記住,這些程式碼來自 Hello World App,但是我們沒有用到 watchOS App 上的 button。所以我只是註釋了原始功能的程式碼。

結論

以上就是如何使用 Watch Connectivity 的 Application Context 方式進行後臺傳輸的教程。向手機端回傳資料也是完全相同的,因為它們具有同樣的代理回撥和屬性。雖然在那種情況下,你可能還需要根據實際情況檢查是否存在與該裝置配對的 Apple Watch 或者 Watch 上是否安裝了對應的 app。

正如我之前提到的,在 ViewController / InterfaceController 中執行所有程式碼可能不是最好的主意,但這只是為了簡單地展示如何使用 API​​。我個人非常喜歡在自己的 Watch Connectivity manager 例項中執行這些操作。所以我強烈建議你閱讀 Natasha The Robot 的文章 WatchConnectivity: Say Hello to WCSession,並關聯他的 GitHub Gist。這將對你使用 WatchConnectivity 很有幫助。

我希望本文能對你有所幫助。如果有幫到你,請不要猶豫,在 Twitter 或者你選擇的社交媒體上分享這篇文章,每個分享對我都是幫助。當然,如果你有任何疑問,請隨時通過聯絡頁面或 Twitter @CodingExplorer 與我聯絡,我會看看我能做些什麼。謝謝!

來源

相關文章