這一篇筆記主要是要記錄如何在 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 機制讓接收方可以回應
  • 傳送的資訊是 DictionaryData

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