假设你已经用uni-app开发好了一个iOS端的app,现在想要开发一个配套的apple watch app。改怎么去开发呢?是不是一头雾水,这篇文章就会介绍一些apple watch app开发的知识以及如何在uni-app开发的iOS app基础上去开发配套的watch app。
一、apple watch 开发知识
apple watch app类型
首先,我们来了解下apple watch app有哪些类型。
我们在新建一个WatchOS app的项目时可以看到,watch app有2种类型。一个是独立的watch app(Watch-only app),一个是依赖iOS app的watch app(with Companion iOS App),那么他们有什么区别呢?我将从几个不同的角度去介绍:
- 安装
watch-only的只能通过apple watch上的appStore去下载安装
with iOS App的,可以通过手机上Watch应用去安装可使用的watch app,当然也可以通过appStore下载
- 开发
watch-only新建项目时只有watchKit app和watchKit extension。
with iOS App的则会有一个iOS app的target
UI框架说明
watchKit app虽然可以通过UIKit,使用swift或者OC来开发,但是官方建议的是使用SwiftUI这个新的UI框架开发。因为 SwiftUI 具有声明式编程、更简洁的语法、更好的跨平台性以及对数据绑定和响应式编程的良好支持等优点,特别适合手表、VR眼镜这种屏幕较小、交互相对简单的设备。
swiftUI的语言和swift还是有较大的区别的,UI控件的使用、生命周期的管理等都大不一样,还是需要各位同学去系统的学习一下。
数据请求类型
有一部分watch app是工具类的App(比如闹钟),不需要获取网络数据。
有一部分watch app是只需要获取健康数据(步数、心率、睡眠等),通过原生的HealthKit即可获取。
有一部分watch app需要联网获取在线数据,就需要通过Alamofire等网络库或者原生的URLSession来实现数据请求。
还有一种特殊的数据获取方式,就是通过Watch Connectivity实现watch app和手机app数据交换,数据可以通过手机app请求拿到(在线或者其他方式),然后传输给watch app。watch app也可以传输数据给手机app。下面会详细讲下这个Watch Connectivity。
Watch Connectivity
这里我们是有2个设备,一个watch,一个iphone。对应项目里是有一个app的target,一个watchKit的target。
WCSessionManager单例封装
首先我们来封装一个Manager来管理数据的发送、接收以及最开始的初始化。
通过shared实现单例访问,持有session属性和一个消息回调callBack。同时需要遵循WCSessionDelegate实现相关代理方法(状态变化、接收消息等)。
session是WCSession类型,整个过程的数据交换都是通过WCSession来实现的。
import WatchConnectivity
final class WCSessionManager: NSObject {
static let shared = WCSessionManager()
var session = WCSession.default
var callBack: ((NSDictionary)->Void)!
private override init() {
super.init()
if WCSession.isSupported() {
session.delegate = self
session.activate()
}
}
func initWithCallBack(callback: @escaping (NSDictionary) -> Void) {
let sessionManager = WCSessionManager.shared
sessionManager.callBack = callback
}
func sendMessage(message: [String: Any]) {
session.sendMessage(message, replyHandler: nil)
}
}
// MARK: - WCSessionDelegate
extension WCSessionManager: WCSessionDelegate {
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) {
print("\(#function): activationState = \(session.activationState.rawValue)")
}
func sessionDidDeactivate(_ session: WCSession) {
// Activate the new session after having switched to a new watch.
session.activate()
}
func sessionWatchStateDidChange(_ session: WCSession) {
print("\(#function): activationState = \(session.activationState.rawValue)")
}
func session(_ session: WCSession, didReceiveMessageData messageData: Data, replyHandler: @escaping (Data) -> Void) {
print("didReceiveMessageData")
}
#endif
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
// 处理激活完成
print("\(#function): activationState = \(session.activationState.rawValue)")
}
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
// 处理接收到的消息
print("收到message: \(message)")
}
func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
// 处理需要回复的消息
print("收到message: \(message)")
replyHandler(["status": "received"])
}
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
print("收到userInfo: ")
print(userInfo)
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
print("收到context: ")
print(applicationContext)
}
}
初始化并设置回调处理
1、在iOS app对应target的AppDelegate(或者其他页面中也行)调用initWithCallBack实现初始化并设置收到消息后回调处理。initWithCallBack内部会调用.shared从而触发init方法,再通过内部session.delegate = self和session.activate()完成初始化。
2、在watchkit app对应的target的里的AppDelegate文件中调用initWithCallBack实现初始化并设置收到消息后回调处理。这里需要注意watchKit app里本身没有AppDelegate文件,需要自己新建并且在main主入口中关联。
新建:
import WatchKit
import WatchConnectivity
class AppDelegate: NSObject, WKApplicationDelegate {
override init() {
super.init()
assert(WCSession.isSupported(), "This sample requires a platform supporting Watch Connectivity!")
WCSessionManager.shared.initWithCallBack { data in
}
}
}
关联:
import SwiftUI
@main
struct WatchApp_Watch_AppApp: App {
@WKApplicationDelegateAdaptor var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
3、链接状态介绍。我们在调用初始化的方法后,会进入到对应的代理方法中,返回WCSession的状态session.activationState。
由 WCSessionActivationState
枚举定义:
- activated:会话已激活,可正常通信。
- inactive:设备已配对但会话未激活(需调用
activate()
方法)。 - notPaired:设备未配对,无法通信。
- notConnected:设备已配对但未连接(如蓝牙断开)。
数据发送
我们先来了解下Watch Connectivity发送数据有哪几种方式:
- 短消息(Messages):实时发送少量数据(如布尔值、字符串、字典)。
- 用户信息(User Info):传输字典类型数据,支持后台异步传输。
- 文件传输(Transfer File):发送较大文件(如图片、视频)。
- 数据更新(Update Context):更新共享的上下文数据(如用户设置、当前状态)。
一般我们只用到前面2种,可以直接调用WCSessionManager中的方法实现发送:
WCSessionManager.shared.sendMessage(["a":"aa"])
WCSessionManager.shared.transferUserInfo(["b":"baba"])
二、绑定uni-app开发的iOS App
前面第一部分介绍了apple watch app开发的部分。那么怎么将watch app绑定到用uni-app开发的App中呢?
一开始我先为iPhone和iWatch分别创建了2个独立的项目,按规则设置bundleID、AppGroups等,测试发现没办法进行通讯。运行会提示对方App未安装,查询资料是没有设置target dependencies,但是2个分开的项目没办法设置target dependencies。所以还是得在一个项目里创建2个target来分别开发2个端。
于是就想到将uni-app项目通过本地离线打包,得到一个可以在原生xcode中运行的项目工程。期间研究离线打包的知识也是花了一点时间,而且由于uni-app用到了UTS插件,还需要将这些插件打包成iOS原生的framework加到离线工程里才能使用。就在向官方技术咨询uts插件打包framework的问题时,了解到最新的HBuilderX支持Watch App产物添加。也就是可以把原生打包出来的ipa中的watch app部分,配置到uni-app工程中,再通过云打包,可以得到包含watchKit app的iOS app。这简直太香了,于是我就放弃当前离线工程的方式,开始新的方案。
现在开始介绍新的方案,也是最终方案:
- 首先在Xcode中新建一个iOS app工程,就和开发普通App一样即可。
- 然后查看项目Target并新建一个Target,选WatchOS --> App,类型选择“Watch App for Existing iOS App”
- 然后修改watchApp 这个target里的bundleId,注意这里有严格的格式要求。比如iOS App里bundleID是com.***.app,那么watch里就要用com.***.app.watchkitapp。
- 然后再配置App Groups,这一步可以参考苹果文档或者网络上教程。
- 完成上面这些操作,就算是创建好了一个基础的框架了。接下来就是开发功能相关的了,此处省略N万字。
- 开发完功能后,就是打包了。Scheme选中iOS App这个,和之前开发iOS App普通打包一样,打包得到ipa。然后解压ipa,找到里面watch文件夹,复制watch文件夹里面的.app 文件。
- 关联watch app到 uni-app工程。在 uni-app/uni-app x 项目原生资源目录 “nativeResources” 的 “ios” 目录下创建 “Watch”子目录,并添加 Watch App 二级制文件,结构如下图。可以在ios-watch.json中配置云打包时重签名设置。
- 云打包制作自定义基座,安装App调试,测试是否同步有可使用的watch App。
至此,在uni-app开发的App基础上开发一个配套的watch app方案就介绍完了。