這一篇筆記主要是要記錄如何在 iPhone app 跟 watchOS app 之間傳遞資料。
iPhone 跟 Apple Watch 的溝通方式
iPhone app 跟 Watch app 可以透過設定相同的 App Group 共享檔案,也可以透過 WatchConnectivity 溝通,App Group 的方式沒什麼好說的,所以來紀錄 WatchConnectivity 的作法。它主要分為兩種方式:
Interactive Messaging
使用這個模式溝通,iPhone 跟 Watch 的 app 需要同時開著,這樣才可以傳遞即時資訊。主要重點如下:
- 它是即時的
- 傳送方應該要先檢查接收方是否
reachable
再決定要不要傳資訊 - 提供
reply handler
機制讓接收方可以回應 - 傳送的資訊是
Dictionary
或Data
Background Transfer
使用這個模式溝通,傳送方在傳遞資訊時,接收方可以不用開啟。系統會自行決定在適合的時機在背景傳完資訊,等到接收方啟動的時候再告知接收方。這個模式又可以分成三種模式:
Application Context Mode
- 傳送的資料是
Dictionary
- Transfer Queue 裡頭舊的資料會被新的蓋過,接收方只會拿到最新的資料
- 接收方的
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String: AnyObject])
只會被呼叫一次
User Information Transfer Mode
- 傳送的資料是
Dictionary
- FIFO 進 Transfer Queue,新資料不會蓋過舊資料
- 傳完之前,傳送方可以取消任意一筆傳送
- 傳完之前,接收方的
func session(session: WCSession, didReceiveUserInfo userInfo: [String: AnyObject])
會根據 FIFO 順序被多次呼叫
File Transfer Mode
- 傳檔案
- 機制跟
User Information Transfer Mode
一樣 - 收到的檔案會暫存在
Document/Inbox
,delegate function scope 結束就會自動被系統刪除 - 為了避免檔案被刪除,要在 delegate function scope 結束前把檔案搬到其他地方 (可參考 sample code)
Sample Code
檔案傳送方
import WatchConnectivity
class Sender: NSObject {
private let session = WCSession.default
override init() {
super.init()
if WCSession.isSupported() {
// 要先設定 delegate 才能 activate
session.delegate = self
session.activate()
}
}
func transferFile() {
guard WCSession.isSupported() else {
print("WCSession not supported!")
return
}
// 傳遞時可以附帶 metadata dictionary
session.transferFile(fileURL, metadata: nil)
}
}
extension Sender: WCSessionDelegate {
// 為了支援配對多隻手錶的可能,這兩個 function 是必要的
func sessionDidBecomeInactive(_ session: WCSession) {
}
func sessionDidDeactivate(_ session: WCSession) {
}
}
檔案接收方
import WatchConnectivity
class Receiver: NSObject {
private let session = WCSession.default
override init() {
super.init()
if WCSession.isSupported() {
session.delegate = self
session.activate()
}
}
}
extension Receiver: WCSessionDelegate {
func session(_ session: WCSession, didReceive file: WCSessionFile) {
/// 要在這個 delegate function 結束前把檔案搬到其他地方,
/// 不然會被系統自動刪除。
let fileManager = FileManager.default
var docDir = try! fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
docDir.appendPathComponent("Received.bundle", isDirectory: true)
/// Create destination directory if it doesn't exist.
if !fileManager.fileExists(atPath: docDir.path, isDirectory: nil) {
do {
try fileManager.createDirectory(at: docDir, withIntermediateDirectories: true, attributes: nil)
} catch {
print("Create directory error: \(error.localizedDescription)")
return
}
}
let fileName = file.fileURL.lastPathComponent
let destURL = docDir.appendingPathComponent(fileName)
/// Replace existing file so that we make sure to use latest received file.
if fileManager.fileExists(atPath: destURL.path) {
do {
try fileManager.removeItem(at: destURL)
} catch {
print("Removing existing file error: \(error.localizedDescription)")
}
}
do {
try fileManager.moveItem(at: file.fileURL, to: destURL)
} catch {
print("Moving file error: \(error.localizedDescription)")
}
}
}
踩到的坑
- File Transfer Mode 只能傳檔案不能傳資料夾,所以看起來像檔案但其實是資料夾的 Bundle/App file 也不能傳
- 一定要用實機測試,模擬器無法成功傳檔案,這是個存在已久的 bug