WatchOS开发教程之四: Watch与 iPhone的通信和数据共享

WatchOS 开发教程系列文章:
WatchOS开发教程之一: Watch App架构及生命周期
WatchOS开发教程之二: 布局适配和系统Icon设计尺寸
WatchOS开发教程之三: 导航方式和控件详解
WatchOS开发教程之四: Watch与 iPhone的通信和数据共享
WatchOS开发教程之五: 通知功能开发
WatchOS开发教程之六: 表盘功能开发


Apple Watch与iPhone之间的通信

在第一篇文章Watch App架构及生命周期的最后, 我们提到过: 避免长时间运行的任务。由于与 Watch App的交互通常很简短, 因此在长时间运行的任务完成之前, 可能会暂停 WatchKit Extension。 执行任何长时间运行任务的最佳解决方案是在 iOS App中执行该任务, 然后将数据传输给 Apple Watch。

所以, 今天来说一说Apple Watch与iPhone之间的通信是如何实现的。

WatchConnectivity框架

在 WatchOS中有个WatchConnectivity框架, 是专门负责 WatchOS与 iOS之间的通信的。使用Connectivity框架在 WatchKit Extension和 iOS App之间进行通信。该框架提供了两个进程之间的双向通信,并允许在前台或后台进行数据和文件的传输。

Connectivity框架提供了几种在 iOS 和 WatchKit Extension之间发送数据的选项, 每个选项都用于不同的用途。大多数选项在后台执行单向数据传输,而且是提供更新的便捷方式。前台传输让你的应用立即发送消息并等待回复。

对于大多数类型的传输,您提供一个NSDictionary包含要发送的数据的对象。字典的键和值必须都是属性列表类型,因为数据必须序列化并以无线方式发送。属性列表类型是指Foundation框架中的NSNumbeNSStringNSArrayNSDictionaryBoolNSDateNSData等数据类型。如果需要包含非属性列表类型的类型,请将它们打包到NSData对象中,或者在发送之前将它们写入文件。此外,您发送的词典应该是紧凑的,并且只包含您需要的数据。保持字典较小可确保它们快速传输,并且不会在两台设备上消耗太多电量。

WCSession

WatchConnectivity框架中主要是通过WCSession类进行数据传输的。来看下WCSession这个类, 它有一个default单例。default session用于在两个对应应用程序(即 iOS App及 WatchKit Extension)之间进行通信。Session提供了发送,接收和跟踪状态的方法。

您的 iOS App和 WatchOS App必须在执行期间的某个时刻创建和配置此类的实例。当两个会话对象都处于激活状态时,这两个进程可以通过发送消息立即进行通信。当只有一个Session处于激活状态时,Session仍可以发送更新和传输文件,但这些传输在后台机会性地发生。

Session的配置和激活

在尝试发送消息或获取有关连接状态的信息之前,必须配置并激活Session。 在激活Session之前,可需要先进行一个检查当前 iOS设备是否支持Connectivity框架, 方法就是调用isSupported()方法。

在 iOS App中检查是否支持Connectivity框架并激活Session的代码如下:

func configureWCSession() {
    if #available(iOS 9.0, *) {
        // Some properties can be checked only for iOS Device
        // WCSession.default.isPaired
        // WCSession.default.isWatchAppInstalled
        // WCSession.default.isComplicationEnabled
        if WCSession.isSupported() {
            let session = WCSession.default
            session.delegate = self
            session.activate()
        } else {
            // Current iOS device dot not support session
        }
    } else {
        // The version of system is not available
    }
}

WCSession类中, 还有一些属性是只能在 iOS App中使用的。比如, isPaired, isWatchAppInstalled, isComplicationEnabled, remainingComplicationUserInfoTransfers, watchDirectoryURL。这些都是仅仅在 iOS App中可用的, 都是标示当前设备的某些状态的。所以, 我们要在通信前利用好这些属性。

在激活WatchKit Extension的Session前, 不必检查是否支持Connectivity框架, 因为 WatchOS一定支持Connectivity框架。所以, 在 WatchKit Extension中Session的配置和激活就相对简单一些:

func configureWCSession() {
    // Don't need to check isSupport state, because session is always available on WatchOS
    // if WCSession.isSupported() {}
    let session = WCSession.default
    session.delegate = self
    session.activate()
}

选择合适的通信方式

WatchKit Extension与 iPhone间的通信方式有很多种, 可以分为前台实时传输和后台不定时传输两大传输类型。前台传输, 是实时传输, 消息字典传输和消息数据传输。后台传输又分为覆盖式传输, 队列式传输。队列式传输又分为字典传输, 文件传输, 表盘数据传输。一张图把这一切说清楚:

WatchOS开发教程之四: Watch与 iPhone的通信和数据共享_第1张图片

后台传输

后台传输是异步执行的, 当发送方的应用退出时,后台传输将会继续。对应的接收方应用不需要运行也可以继续进行后台传输。并且在 WatchKit Extension与 iPhone进行传输的方法中, 所有的后台传输都是不定时传输。不定时意味着, 数据不一定会立即传输, 而是系统将在适当的时间传输内容。当然包括上面所说的在应用程序退出之后发生, 甚至是在双方应用都不运行时发生。接收方没有运行但传输成功了, 下次启动时将会触发相应的代理方法。

后台覆盖式传输

后台传输中覆盖式的传输意味着, 当你进行数据传输时, 如果第一次发送的数据还没有送出去, 在此时进行第二次数据传递, 将会覆盖第一次的数据。这时数据接收方接收的数据只会有第二次的, 第一次的数据会丢失。

Connectivity框架的通信方法中, 后台覆盖式传输只有一个方法, 在 Objective-C中就是updateApplicationContext:error:方法, 在 Swift中函数如下:

open func updateApplicationContext(_ applicationContext: [String : Any]) throws

一般使用该方法将最近的状态信息传递给对方, 且只有在Session处于激活状态时才能调用此方法, 系统将会在适当的时间传输内容。使用此方法传输后, 发送的数据会存储在applicationContext属性中, 而最新接收的数据会存储在receivedApplicationContext属性中。

接收方可以遵从 WCSessionDelegate的协议, 在这些代理方法中, 有一个是与上面的方法成对存在的。接收方若实现了下面的代理方法, 当数据发送方在调用上面的方法后, 将会触发它们:

optional public func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any])

后台队列式传输

后台传输中队列式的传输意味着, 后一次的传输不会覆盖前一次所传输的数据。系统会把所有的数据按照次序进行发送。在Connectivity框架的通信方法中, 后台队列式有三个方法, 后台队列式字典传输, 后台队列式文件传输, 后台队列式表盘数据传输。

后台队列式字典传输

在 Objective-C中后台队列式字典传输的方法是transferUserInfo:, 在 Swift中函数如下:

open func transferUserInfo(_ userInfo: [String : Any] = [:]) -> WCSessionUserInfoTransfer

此方法可以传输一个字典, 且只有在Session处于激活状态时才能调用此方法。系统将userInfo字典按序排入队列, 并在适当的时候将其传输到接收方应用中。你还可以通过outstandingUserInfoTransfers属性来获取仍在传输中(即未被接收方取消, 失败或已接收)的userInfo数组。

接收方可以遵从 WCSessionDelegate的协议, 在这些代理方法中, 有两个是与上面的方法成对存在的。接收方若实现了下面的代理方法, 当数据发送方在调用上面的方法后, 将会触发它们:

optional public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:])

optional public func session(_ session: WCSession, didFinish userInfoTransfer: WCSessionUserInfoTransfer, error: Error?)

后台队列式文件传输

在 Objective-C中后台队列式文件传输的方法是transferFile:metadata:, 在 Swift中函数如下:

open func transferFile(_ file: URL, metadata: [String : Any]?) -> WCSessionFileTransfer

此方法可以传输一个文件和一个可选字典, 且只有在Session处于激活状态时才能调用此方法。你还可以通过outstandingFileTransfers属性来获取仍在传输中(即未被接收方取消, 失败或已接收)的userInfo数组。

接收方可以遵从 WCSessionDelegate的协议, 在这些代理方法中, 有两个是与上面的方法成对存在的。接收方若实现了下面的代理方法, 当数据发送方在调用上面的方法后, 将会触发它们:

optional public func session(_ session: WCSession, didReceive file: WCSessionFile)

optional public func session(_ session: WCSession, didFinish fileTransfer: WCSessionFileTransfer, error: Error?)

这里需要注意的是, 接收到的文件存放在本地临时路径Documents/Inbox/中, 代理方法session(_ session: WCSession, didReceive file: WCSessionFile)结束后系统会将文件删除。所以你需要接收到文件后, 立即对其进行读取或者移动。

后台队列式表盘数据传输

在 Objective-C中后台队列式表盘数据传输的方法是transferCurrentComplicationUserInfo:, 在 Swift中函数如下:

open func transferCurrentComplicationUserInfo(_ userInfo: [String : Any] = [:]) -> WCSessionUserInfoTransfer

此方法涉及到 WatchOS的表盘功能也就是Complication功能, 且只适用于 iPhone向 WatchKit Extension发送表盘功能相关的数据。此方法将包含表盘功能的最新信息的字典userInfo排入队列中。并且只有在Session处于激活状态时才能调用此方法。

与之相关的属性有remainingComplicationUserInfoTransfers, 它标示这transferCurrentComplicationUserInfo:方法的剩余调用次数。在系统开始将表盘userInfo作为常规userInfo传输之前。 如果此属性为0,则表盘userInfo将作为常规userInfo传输。 当 Watch应用未启用表盘功能时, 其计数也为0。

如果启用了表盘功能, 系统将立即尝试向 WatchKit Extension传输此userInfo, 且传输为高优先级。一旦收到当前的表盘功能的userInfo, 系统将在后台启动 WatchKit Extension并允许其更新并表盘内容。如果当前用户信息无法传输(即设备断开连接, 超出后台启动预算等), 它将在outstandingUserInfoTransfers队列中等待, 直到下一个合适的时间。

需要注意的是, 在outstandingUserInfoTransfers队列中只能有一个当前的表盘的userInfo。如果当前表盘userInfo还在队列当中(等待传输), 并且再次传输了一个新的userInfo, 则新userInfo将被标记为当前需要传输的userInfo。而先前的userInfo将被取消标记, 那么无论如何它都将一直存在于outstandingUserInfoTransfers队列中了。

表盘功能传输中接收方(即 Watch端)可以实现的代理与后台队列式字典传输的代理相同。

前台传输

在 WatchKit Extension与 iPhone进行传输的方法中, 前台传输是实时的, 且是队列式的传输方式。具体有两种方法, 一种是传输消息字典, 另一种是传输消息数据。

前台消息字典传输

在 Objective-C中前台消息字典传输的方法是sendMessage:replyHandler:errorHandler:, 在 Swift中函数如下:

open func sendMessage(_ message: [String : Any], replyHandler: (([String : Any]) -> Swift.Void)?, errorHandler: ((Error) -> Swift.Void)? = nil)

此方法传入一个消息字典, 一个处理接收方回复的block, 以及一个错误处理block。消息的传递是异步的、高优先级的, 且只有在会话处于活动状态时才能调用此方法。如果指定了处理接收方回复的block, 则该block也会在后台线程上异步执行。

需要注意的是, 从 WatchKit Extension激活并运行时调用此方法会在后台唤醒相应的 iOS App并使其可访问。但若从 iOS App调用此方法则不会唤醒相应的WatchKit Extension。如果调用此方法时接收方无法访问(即isReachable是 false), 则会执行errorHandlerblock并显示相应的错误。

那么isReachable什么时候是true呢? 对于 WatchKit Extension来说, iOS设备在范围内, 因此可以进行通信并且 WatchKit Extension在前台运行,或者在后台运行时具有高优先级(例如, 在锻炼会话期间或当表盘加载其初始时间轴数据时); 对于 iOS来说, 配对且激活的 Apple Watch在范围内, 相应的WatchKit Extension正在运行。只要这样isReachable属性才会为true。

而且当传输的消息字典中包含非属性列表数据类型, 也会调用errorHandlerblock。其他的类型数据应该用你下面的方法来进行传输。

前台消息数据传输

在 Objective-C中前台消息字典传输的方法是sendMessageData:replyHandler:errorHandler:, 在 Swift中函数如下:

open func sendMessageData(_ data: Data, replyHandler: ((Data) -> Swift.Void)?, errorHandler: ((Error) -> Swift.Void)? = nil)

此方法与消息字典传输的方法的区别在于所传输的主体内容为Data类型。包含非属性列表数据类型的传输, 就需要使用此方法了, 否则用上面方法将会报错。

传输数据处理

WatchOS开发教程之四: Watch与 iPhone的通信和数据共享_第2张图片

当你选择了合适的方式进行数据通信后, 就是处理这些接收到的数据和处理接收方回复了。核心就是在WCSessionDelegate中不同传输方式对应的不同代理方法中去处理。其实, 这些上面每个方法中已经详细说过了, 但为了知识的结构这里还是有必要提一下的, 详细的这里不赘述了。

代码描述

以前台消息字典传输为例, 由 WatchKit Extension向 iOS App发送消息。 WatchKit Extension中代码如下:

if !WCSession.default.isReachable {
    let action = WKAlertAction(title: "OK", style: .default) {
        print("OK")
    }
    presentAlert(withTitle: "Failed", message: "Apple Watch is not reachable.", preferredStyle: .alert, actions: [action])
    return
} else {
    // The counterpart is not available for living messageing
}

let date = Date(timeIntervalSinceNow: 0.0)
let message = ["title": "Apple send a messge to iPhone", "watchMessage": "The Date is \(date.description)"]
WCSession.default.sendMessage(message, replyHandler: { (replyMessage) in
    print(replyMessage)
    DispatchQueue.main.sync {
        self.contentLabel.setText(replyMessage["replyContent"] as? String)
    }
}) { (error) in
    print(error.localizedDescription)
}

WatchKit Extension中实现对应代理方法, 以处理 iOS App发回的回复数据:

func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    DispatchQueue.main.sync {
        contentLabel.setText(message["iPhoneMessage"] as? String)
    }
}

iOS App也应实现对应代理, 以处理接收到的数据:

@available(iOS 9.0, *)
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    print(message)
    replyHandler(["title": "received successfully", "replyContent": "This is a reply from iPhone"])
}

数据共享

Watch App和 WatchKit Extension间数据共享

在运行时可以使用共享App Group在 Watch App和 WatchKit Extension之间共享媒体文件。 App Group创建一个多个进程可以访问的安全容器。 通常每个进程都在自己的沙箱环境中运行, 但是App Group允许两个进程共享一个公共目录。

如何使用共享App Group

1.在Xcode中打开项目的Capabilities选项卡。
2.启用App Group功能。这将会添加一个entitlement file到指定的Target,并将一个唯一标识的App Group添加到该文件中。

WatchOS开发教程之四: Watch与 iPhone的通信和数据共享_第3张图片

3.且需要注意Watch App和 WatchKit Extension的 Target必须都启用相同的App Group
4.访问其中内容时, 使用NSFileManagercontainerURLForSecurityApplicationGroupIdentifier:方法取得文件的URL。

文件存储

WatchKit Extension的存储目录与iOS App的存储目录具有相同的基本结构。将用户数据和其他关键信息放在Documents目录中。如果将文件放在Caches目录中, 磁盘空间量较低时系统会删除它们。

数据备份

Apple Watch不会自动备份WatchKit Extension保存的文件。如果需要备份 Watch App中的数据, 则必须将该数据明确传输回iOS App并将其保存在那里。

iCloud

从 WatchOS 3开始, WatchKit Extension可以直接与CloudKit和其他iCloud技术进行通信。

相关资料:
Sharing Data
WatchOS 开发教程源码:Watch-App-Sampler

你可能感兴趣的:(Apple,Watch开发)