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

Posted DCSnail-蜗牛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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间的通信方式有很多种, 可以分为前台实时传输和后台不定时传输两大传输类型。前台传输, 是实时传输, 消息字典传输和消息数据传输。后台传输又分为覆盖式传输, 队列式传输。队列式传输又分为字典传输, 文件传输, 表盘数据传输。一张图把这一切说清楚:

后台传输

后台传输是异步执行的, 当发送方的应用退出时,后台传输将会继续。对应的接收方应用不需要运行也可以继续进行后台传输。并且在 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类型。包含非属性列表数据类型的传输, 就需要使用此方法了, 否则用上面方法将会报错。

传输数据处理

当你选择了合适的方式进行数据通信后, 就是处理这些接收到的数据和处理接收方回复了。核心就是在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添加到该文件中。

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

以上是关于WatchOS开发教程之四: Watch与 iPhone的通信和数据共享的主要内容,如果未能解决你的问题,请参考以下文章

WatchOS开发教程之一: Watch App架构及生命周期

WatchOS开发教程之六: 表盘功能开发

WatchOS开发教程之六: 表盘功能开发

WatchOS开发教程之五: 通知功能开发

WatchOS开发教程之五: 通知功能开发

WatchOS开发教程之三: 导航方式和控件详解