iOS开发SwiftUI

WCSession在iPhone 和 Watch之间传递信息

WCSession:https://developer.apple.com/documentation/watchconnectivity/wcsession

WatchConnectivity框架实现iOS应用程序与其配对的watchOS应用程序之间的双向通信。接下来几篇我们就一起看一下这个框架。

WatchConnectivity是watchOS2里iPhone与AppleWatch通信的基础框架,下面先看一下该框架的基本信息。

使用此框架在iOS应用程序和配对的watchOS应用程序的WatchKit扩展之间传输数据。 您可以传递少量的数据或整个文件。 您也可以使用这个框架触发您的watchOS应用程序的复杂化更新。

从您的应用程序启动传输后,系统将负责传输任何数据。 当接收应用程序处于非活动状态时,大多数传输都会在后台进行。当应用程序被唤醒时,它会被通知任何在inactive状态下到达的数据。 两个应用程序都处于活动状态时,实时通信也是可能的。


初始化设置

WatchConnectivity的原理是iPhone伴侣应用和AppleWatch上运行的WatchKit Extension之间通过WCSession会话进行通信。当前Controller若需要响应WatchConnectivity的消息需要遵循WCSessionDelegate协议。

初始化时需要激活会话代理(iPhone应用和WatchKitExtension里都要激活),代码如下: 

if WCSession.isSupported() { 

   let session=WCSession.defaultSession() 

   session.delegate=self 

   session.activateSession() 

}

会话状态

为了了解iPhone应用与Watch应用的会话状态,我们可以通过一些属性去判断,注意这些操作只能在iPhone应用里进行。

  • 检查是否配对

AppleWatch是否与iPhone配对,可以通过session.paired属性的布尔值进行判断

  • 检查watchapp是否已安装

即使已配对,watchapp也可能未安装成功,通过session.watchAppInstalled属性的布尔值可以得知此状态。另外,watchapp安装成功就会在手表上建立相应app的目录,因此session.watchAppInstalled==false效果等同于watchDirectoryURL != nil。

  • 3.检查是否开启表盘组件功能

若watchapp支持complication表盘组件,那么session.complicationEnabled值为true。


通信

WatchConnectivity框架的通信方式有两种模式,一种是后台传输,另一种是交互式消息


后台传输模式

后台传输模式是最常用的通信模式,面向内容与用户交互,主要用于传输非即时的内容,体现在内容可由操作系统智能传输(操作系统允许发送方可退出,选择传输时机,支持接收者下次启动时发送),并将内容以队列方式发送。 后台传输一般分三种类型: 

  • 1. Application context 

发送方代码示例

 do {
    let context=//最新内容,初始化context
    try WCSession.defaultSession().updateApplicationContext(context)

    } catch {

  } 

接收方需响应以下代理方法:

func session(session:WCSession,didReceiveApplicationContext:applicationContext:[String:AnyObject]){} 

ApplicationContext传输数据常见于传输单个字典类型数据的情况,非常适合仅需要信息子集的AppleWatch应用。这过程不会即刻发送,但会在对应的app唤醒的时候发送。

  • 2. Userinfo transfer 

Userinfo方式与ApplicationContext相比能够传输更复杂的数据。 

发送方示例代码:

let userInfo= //待传输的内容

let userInfoTransfer = WCSession.defaultSession().transferUserInfo(userInfo)

userInfoTransfer传输器封装了待传数据,并且你可以通过代码控制来取消传输。 


未传输的内容可以这样获取:

let transfer=WCSession.defaultSession().outstandingUserInfoTransfer(userInfo)


接收方通过以下回调方法进行处理:

func session(session:WCSession,didReceiveUserInfo userInfo:[String:AnyObject]) {//处理接收的userInfo}

Userinfo transfer适合内存内容的传输,并且支持访问队列里的未传输内容。

  • 3. File transfer 

File transfer面向文件,API使用上和Userinfo transfer很像,支持队列,支持未完成内容的访问,需要附加元数据信息。 

发送方示例代码如下:

let url = //文件地址
let metadata = //字典形式元数据
let fileTransfer = WCSession.defaultSession().transferFile(url,metadata:metadata)

接收方代码如下:
func session(session:WCSession,didReceiveFile file:WCSessionFile) {//处理接收的File}

交互式消息

交互式消息能够为iPhone和AppleWatch间提供实时的通讯功能。

使用前提:1.设备间能够无线联通;2.应用之间能够联通,这意味着AppleWatch端的程序必须前台运行,即session的reachable值为true。

与后台传输模式的一个值得注意的区别是:若iOS应用未启动AppleWatch上运行的WatchKit扩展能够启动iOS应用

交互式消息方式可以传输两种数据类型的消息:

1.字典
func sendMessage(message:,replyHandler:,errorHandler:)

2.数据 
支持可序列化的自定义数据
func sendMessageData(data:,replyHandler:,errorHandler:)

用于数据即时传输到对应app的方法。调用这个方法发送的数据会进入一个队列,按照进入队列的先后顺序来发送出去。如果是从watch向iOSapp发送数据并且该iOSapp没在运行的话,那么接收数据的iOSapp将会在后台被唤醒。如果你从iOSapp发送数据而watchapp没有运行的情况下,errorHandler就会被调用。接收数据的app会通过WCSession的委托方法session(_:didReceiveMessageData:replyHandler:)方法来接收。

在以下方法里进行接收时的回调处理

func session(session:WCSession,didReceiveMessage message:[String:AnyObject],replyHandler:([String:AnyObject]) -> Void){//消息处理并返回}    

Demo

iPhone端代码

  • AppDelegate.swift
import UIKit
import WatchConnectivity

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        if WCSession.isSupported() {
            WKSession = WCSession.default
            WKSession?.delegate = self
            WKSession?.activate()
        }

        // Override point for customization after application launch.
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }

}

extension AppDelegate: WCSessionDelegate {

    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        if activationState == WCSessionActivationState.activated {
            print("App Session Activated")
        } else if activationState == WCSessionActivationState.inactive {
            print("App Session Inactive")
        } else if activationState == WCSessionActivationState.notActivated {
            print("App Session Not Activate")
        }
    }

    func sessionDidDeactivate(_ session: WCSession) {

    }

    func sessionDidBecomeInactive(_ session: WCSession) {

    }

    func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {

        let queue = DispatchQueue.main
        queue.sync {
            NotificationCenter.default.post(name: .receiveMsg, object: message, userInfo: nil)
        }

    }

    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
        let queue = DispatchQueue.main
        queue.sync {
            NotificationCenter.default.post(name: .receiveUserInfo, object: userInfo, userInfo: nil)
        }
    }
}

var WKSession: WCSession?


  • ContentView.swift

import SwiftUI

struct ContentView: View {

    @State var textContent: String = ""
    @State var textInput: String = ""
    var isReceiveMsg = NotificationCenter.default.publisher(for: .receiveMsg)
    var isReceiveUserInfo = NotificationCenter.default.publisher(for: .receiveUserInfo)

    var body: some View {

        VStack {
            Text(("from watch:\(self.textContent)"))
                .padding().onReceive(self.isReceiveMsg) { message in
                if message.object != nil {
                    let msgDic = message.object as! [String: Any]
                    self.textContent = msgDic["message"] as! String
                }
            }.onReceive(self.isReceiveUserInfo) { message in
                if message.object != nil {
                    let msgDic = message.object as! [String: Any]
                    self.textContent = msgDic["message"] as! String
                }
            }

            TextField("please input message", text: self.$textInput)

            Button(action: {
                var messageDictionary = Dictionary<String, Any>()
                messageDictionary.updateValue(self.textInput, forKey: "message")
                WKSession!.sendMessage(messageDictionary, replyHandler: { (replayDic: [String: Any]) -> Void in
                    print("reply")
                }, errorHandler: { (error) -> Void in
                        print(error)
                    })
            }) {
                Text("sendMessage")
            }


            Button(action: {
                var messageDictionary = Dictionary<String, Any>()
                messageDictionary.updateValue(self.textInput, forKey: "message")
                WKSession?.transferUserInfo(messageDictionary)
            }) {
                Text("transferUserInfo")
            }.padding(.top, 15)
        }
    }
}


Apple Watch端代码

  • ExtensionDelegate.swift
import WatchKit
import WatchConnectivity

class ExtensionDelegate: NSObject, WKExtensionDelegate {

    func applicationDidFinishLaunching() {
        if WCSession.isSupported() {
            WKSession = WCSession.default
            WKSession?.delegate = self
            WKSession?.activate()
        }
    }

    func applicationDidBecomeActive() {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillResignActive() {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, etc.
    }

    func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
        // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
        for task in backgroundTasks {
            // Use a switch statement to check the task type
            switch task {
            case let backgroundTask as WKApplicationRefreshBackgroundTask:
                // Be sure to complete the background task once you’re done.
                backgroundTask.setTaskCompletedWithSnapshot(false)
            case let snapshotTask as WKSnapshotRefreshBackgroundTask:
                // Snapshot tasks have a unique completion call, make sure to set your expiration date
                snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
            case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
                // Be sure to complete the connectivity task once you’re done.
                connectivityTask.setTaskCompletedWithSnapshot(false)
            case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
                // Be sure to complete the URL session task once you’re done.
                urlSessionTask.setTaskCompletedWithSnapshot(false)
            case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
                // Be sure to complete the relevant-shortcut task once you're done.
                relevantShortcutTask.setTaskCompletedWithSnapshot(false)
            case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
                // Be sure to complete the intent-did-run task once you're done.
                intentDidRunTask.setTaskCompletedWithSnapshot(false)
            default:
                // make sure to complete unhandled task types
                task.setTaskCompletedWithSnapshot(false)
            }
        }
    }

}

extension ExtensionDelegate: WCSessionDelegate {
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        if activationState == WCSessionActivationState.activated {
            print("App Session Activated")
        } else if activationState == WCSessionActivationState.inactive {
            print("App Session Inactive")
        } else if activationState == WCSessionActivationState.notActivated {
            print("App Session Not Activate")
        }
    }

    func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
        WKInterfaceDevice.current().play(WKHapticType.notification)
        NotificationCenter.default.post(name: .receiveMsg, object: message, userInfo: nil)
    }

    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
        let queue = DispatchQueue.main
        queue.sync {
            WKInterfaceDevice.current().play(WKHapticType.notification)
            NotificationCenter.default.post(name: .receiveUserInfo, object: userInfo, userInfo: nil)
        }

    }
}

var WKSession: WCSession?

extension Notification.Name {
    static var receiveMsg: Notification.Name {
        Notification.Name("receiveMsg")
    }

    static var receiveUserInfo: Notification.Name {
        Notification.Name("receiveUserInfo")
    }

}
  • ContentView.swift
import SwiftUI

struct ContentView: View {

    var isReceiveMsg = NotificationCenter.default.publisher(for: .receiveMsg)

    var isReceiveUserInfo = NotificationCenter.default.publisher(for: .receiveUserInfo)


    @State var textContent: String = "message"
    @State var textInput: String = ""

    var body: some View {
        VStack {


            Text(("from iphone:\(self.textContent)"))
                .padding().onReceive(self.isReceiveMsg) { message in
                if message.object != nil {
                    let msgDic = message.object as! [String: Any]
                    self.textContent = msgDic["message"] as! String
                }
            }.onReceive(self.isReceiveUserInfo) { message in
                if message.object != nil {
                    let msgDic = message.object as! [String: Any]
                    self.textContent = msgDic["message"] as! String
                }
            }

            TextField("please input message", text: self.$textInput)

            Button(action: {
                var messageDictionary = Dictionary<String, Any>()
                messageDictionary.updateValue(self.textInput, forKey: "message")
                WKSession!.sendMessage(messageDictionary, replyHandler: { (replayDic: [String: Any]) -> Void in
                    print("reply")
                }, errorHandler: { (error) -> Void in
                        print(error)
                    })
            }) {
                Text("Send Message")
            }

            Button(action: {
                var messageDictionary = Dictionary<String, Any>()
                messageDictionary.updateValue(self.textInput, forKey: "message")
                WKSession?.transferUserInfo(messageDictionary)
            }) {
                Text("transferUserInfo")
            }
        }

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}