若要为 Capacitor 应用构建 Apple Watch 应用,你需要在 iOS 项目中添加原生 watchOS target,用 SwiftUI 编写 UI,并将其连接到你的 Web code,借助 @capgo/capacitor-watch plugin 连接,该 plugin 封装了 Apple 的 WatchConnectivity framework。手机端应用保留其 Capacitor Web 层,而 watch 应用则成为一个轻量级的原生配套应用,负责发送和接收字典。
本指南涵盖在 Capacitor 8 和 Xcode 26 上的完整路径:架构、Xcode target、两侧的 Swift 和 TypeScript code、传输方法选择、模拟器和真机测试,以及 watch app 的发布方式。
为什么 watch UI 是原生的,而手机端保持 Capacitor
watchOS 不附带 WebView。没有 WKWebView 在 watch 上,因此 Capacitor 应用无法在那里运行,也没有插件能改变这一点。你可以做的是拆分职责:
| 层级 | 运行于 | 语言 | 任务 |
|---|---|---|---|
| Capacitor 应用 | iPhone | TypeScript,任选框架 | 业务逻辑、网络、认证、存储 |
@capgo/capacitor-watch |
iPhone | Swift(插件)、JS API | 将 WatchConnectivity 桥接到 JavaScript |
| CapgoWatchSDK | Apple Watch | Swift | 手表端会话处理 |
| Watch 应用 | Apple Watch | SwiftUI | 小屏幕、可瞥视 UI、快速操作 |
实际上,Watch 应用是“依赖型”的:它从手机获取数据。将逻辑保留在已存在 TypeScript 代码中的手机端,只向手表发送其显示所需的数据。
前提条件
- 一个添加了 iOS 平台的 Capacitor 8 应用。如果你仍在使用旧版本,请遵循 Capacitor 8 升级指南 首先。
- Mac 上的 Xcode 26。自 2026 年 4 月起,Apple 要求使用 Xcode 26 上传 App Store,详见 Xcode 26 要求.
- 用于设备测试和分发的 Apple Developer 账户。
- 手机需运行 iOS 15 或更高版本(插件最低要求),手表需运行 watchOS 9 或更高版本(SDK 最低要求)。
- 理想情况下,使用真实的 iPhone 配对真实的 Apple Watch。模拟器有帮助,但无法覆盖所有传输类型。
步骤 1:在手机端安装插件
bun add @capgo/capacitor-watch
bunx cap sync ios
插件加载后 WCSession 立即激活,无需调用 init。首先读取连接状态:
import { CapgoWatch } from '@capgo/capacitor-watch';
const info = await CapgoWatch.getInfo();
console.log({
supported: info.isSupported, // false on iPad, Android without Wear OS, and web
paired: info.isPaired, // an Apple Watch is paired with this iPhone
installed: info.isWatchAppInstalled, // your watch app is on that watch
reachable: info.isReachable, // live messaging is possible right now
state: info.activationState, // 0 notActivated, 1 inactive, 2 activated
});
完整的 API 参考文档位于 插件文档 以及 plugin 页面.
步骤 2:在 Xcode 中添加 watchOS 目标
使用 bunx cap open ios打开。Capacitor 8 个使用 Swift Package Manager 创建的项目 App.xcodeproj;较旧的 CocoaPods 项目 App.xcworkspace。两者均可。
- 选择 文件 > 新建 > Target,选择 watchOS 标签页,然后 应用.
- 给它命名,例如
MyWatch,并使用 SwiftUI 作为界面。 - 在选项面板中,确保 watch app 嵌入到你现有的
Apptarget 中作为其配套 app。正是该设置让 Xcode 将 watch app 嵌入 iPhone build,并设置配套 app 的 bundle identifier。 - 除非现在就需要,否则先别开启通知场景和复杂功能。小组件可以稍后再加。
- 在 prompt 中确认激活新的 scheme。
Bundle identifier 规则
Watch 应用的 Bundle ID 必须以 iPhone 应用的 Bundle ID 开头。如果你的应用是 com.example.shop,Xcode 会建议 com.example.shop.watchkitapp。请保留该前缀。如果之后更改了 iOS 的 Bundle ID,也需同步更改 Watch 的 Bundle ID,否则构建将验证失败,WatchConnectivity 也无法配对这两个应用。
在 签名与能力,为两个目标选择相同的团队。使用自动签名时,Xcode 会为你注册第二个 App ID 和描述文件。
将 CapgoWatchSDK 添加到 watch 目标
- 文件 > 添加包依赖项.
- 输入
https://github.com/Cap-go/capacitor-watch.git. - 从 选择“以下主要版本”
8.0.0. - 当 Xcode 询问要添加哪些产品时,仅选择 CapgoWatchSDK 并将其分配给 watch target,而不是 iOS app。iOS 端已经通过以下方式获取该插件:
cap sync.
第 3 步:用 SwiftUI 编写 watch app
SDK 会暴露 WatchConnector.shared,一个 ObservableObject 带有 published isReachable, isActivated, lastMessage,以及 applicationContext 属性。在 app 启动时激活它:
import SwiftUI
import CapgoWatchSDK
@main
struct MyWatchApp: App {
init() {
WatchConnector.shared.activate()
}
var body: some Scene {
WindowGroup {
OrdersView()
}
}
}
一个小屏幕,显示手机推送的数据,并把操作发回去:
import SwiftUI
import CapgoWatchSDK
struct OrdersView: View {
@ObservedObject private var connector = WatchConnector.shared
@State private var status = ""
var openOrders: Int {
connector.applicationContext["openOrders"] as? Int ?? 0
}
var body: some View {
VStack(spacing: 8) {
Text("\(openOrders)")
.font(.system(size: 44, weight: .bold))
Text("open orders")
.font(.footnote)
.foregroundStyle(.secondary)
Button("Refresh") {
refresh()
}
.disabled(!connector.isReachable)
if !status.isEmpty {
Text(status).font(.caption2)
}
}
}
private func refresh() {
Task {
do {
let reply = try await connector.sendMessage(["action": "refresh"])
status = "Updated \(reply["count"] as? Int ?? 0)"
} catch {
status = "iPhone not reachable"
}
}
}
}
本指南中介绍的 async 的变体 sendMessage 发送时附带回复处理器,因此手机会将其作为 messageReceivedWithReply 事件接收。基于回调的变体 sendMessage(_:replyHandler:errorHandler:) 允许你跳过回复。
如果你更喜欢使用委托回调而不是观察发布的属性,请遵循 WatchConnectorDelegate 并设置 WatchConnector.shared.delegate。它提供了用于消息、带回复处理器的消息、应用上下文、用户信息、可达性和激活的方法。
步骤 4:从 TypeScript 与手表通信
尽早注册监听器,例如在应用启动时,以免错过启动时到达的事件。
import { CapgoWatch } from '@capgo/capacitor-watch';
export async function setupWatchBridge(getOpenOrders: () => Promise<number>) {
// Watch asked for something and is waiting for an answer
await CapgoWatch.addListener('messageReceivedWithReply', async (event) => {
if (event.message.action === 'refresh') {
const count = await getOpenOrders();
await CapgoWatch.replyToMessage({
callbackId: event.callbackId,
data: { count },
});
await pushStateToWatch(count);
}
});
// Fire-and-forget messages from the watch
await CapgoWatch.addListener('messageReceived', (event) => {
console.log('watch says', event.message);
});
await CapgoWatch.addListener('reachabilityChanged', (event) => {
console.log('watch reachable:', event.isReachable);
});
await pushStateToWatch(await getOpenOrders());
}
export async function pushStateToWatch(openOrders: number) {
const info = await CapgoWatch.getInfo();
if (!info.isSupported || !info.isPaired || !info.isWatchAppInstalled) return;
// Latest state only, delivered when the watch app next runs
await CapgoWatch.updateApplicationContext({
context: { openOrders, updatedAt: Date.now() },
});
}
始终回复 messageReceivedWithReply。手表正在等待,未回复的请求会导致手表端出现超时错误。
应该使用哪种传输方式?
WatchConnectivity 提供三个通道,插件将每个通道映射如下:
| 方法 | 交付方式 | 需要手表可达 | 适用场景 |
|---|---|---|---|
sendMessage |
即时 | 是,否则拒绝 | 按钮点击,两个应用同时打开时的 Live Update |
updateApplicationContext |
最新值优先,尽可能时送达 | 否 | 当前状态:计数、设置、已登录用户 |
transferUserInfo |
已排队,按顺序送达 | 否 | 必须全部送达的事件:已记录的训练、已创建的记录 |
一种行之有效的模式:将应用上下文作为手表显示内容的唯一事实来源,使用 sendMessage 仅用于手表 App 打开时的交互,并使用 transferUserInfo 用于任何不能丢失的内容。
设计时需考虑的限制
- Payload 大小。 WatchConnectivity 会拒绝过大的 Payload,并返回
payloadTooLarge,且 Apple 并未为每个通道公布固定的字节数。请保持 Payload 精简:仅传输 ID、计数和短字符串。大型内容请改为从 API 获取。 - Payload 类型。 数据必须兼容 property-list:字符串、数字、布尔值、日期、数据、数组和字典。
null来自 JavaScript 的 values 无效。发送前请移除它们。 - 手机必须正在运行你的应用才能处理事件。 监听器位于 Capacitor JavaScript 运行时中。如果 iOS 已终止应用,事件将无法到达你的 TypeScript,直到应用再次运行。上下文和用户信息由系统排队保留,但请设计手表 UI 使其能从缓存的上下文中工作。
- 配对。 Apple Watch 仅与 iPhone 配对。Android 用户无法使用,且该插件的 Android 实现针对 Wear OS,详见我们的 Wear OS 指南.
- 独立性。 基于 WatchConnectivity 构建的配套应用依赖手机。如果手表需要在远离手机时工作,请添加第二个数据源,例如通过手表的网络连接使用您自己的 API。
在模拟器和设备上测试
您可以在手表模拟器中迭代 SwiftUI 布局,但数据传输需要配对设置。
xcrun simctl list devices
xcrun simctl pair <watch-udid> <phone-udid>
在配对的 iPhone 模拟器上运行 iOS 应用,然后在手表模拟器上运行手表 scheme。两者都必须安装,之后 isWatchAppInstalled 才会变为 true。Apple 指出模拟器不支持 transferUserInfo,因此请在真机上确认该路径。
对于真机,请连接 iPhone,确保手表已启用开发者模式,并选择手表作为手表 scheme 的运行目标。使用 getInfo() 作为您的冒烟测试:
getInfo() 结果 |
含义 |
|---|---|
isPaired: false |
此手机未配对任何手表 |
isWatchAppInstalled: false |
Watch 应用缺失,或 Bundle ID / 配套设置错误 |
isReachable: false,其余为 true |
Watch 应用不在前台或超出范围 |
activationState: 2,全部为 true |
准备就绪 sendMessage |
故障排除
isWatchAppInstalled 保持为 false。 检查 Watch Bundle ID 前缀,确保 Watch target 已嵌入到 App target(Build Phases > Embed Watch Content),然后重新安装两个 App。
来自 watch 的消息始终无法到达 JavaScript。 在 watch 发送前注册监听器,并确认 watch 调用了 WatchConnector.shared.activate() 启动时。在手机端,检查 Xcode 控制台中的插件激活日志。
sendMessage 报错 “Watch is not reachable”。 当 watch app 关闭时属于预期行为。回退到 updateApplicationContext.
replyToMessage 似乎被忽略了。 传入准确的 callbackId 从事件中取值并快速回复。watch 的 reply handler 有超时限制。
构建失败,错误信息为 “Embedded binary’s bundle identifier is not prefixed”。 watch 的 bundle ID 必须以 iOS bundle ID 开头。
Archive 上传因缺少图标被拒绝。 Watch 目标需要自己的应用图标,位于 Assets.xcassets.
发布 Watch 应用
Watch 应用并非独立产品。它嵌入在 iOS 归档中,一同上传,并显示在同一个 App Store Connect 记录里,你可以在 watch 选项卡下添加 Apple Watch 截图。无论你的归档是由本地 Xcode、CI 还是 Capgo 构建,需要为两个 Bundle ID 配置描述文件,并且必须使用 Xcode 26。
更新时请记住一点。 Capgo 实时更新 可以将更改发布到手机的 Web bundle,而无需经过商店审核,这在你的 watch bridge 的 TypeScript 端出现 bug 时非常有用。手表上的 Swift code,以及对原生插件的任何更改,仍需通过 App Store。请为你的消息格式进行版本控制,例如使用 v 字段在每个字典中,以便较新的手机 bundle 能与较旧的手表 build 保持兼容。
总结
一旦接受这种划分,逻辑就很简单了:手腕上用 SwiftUI,其他一切用 Capacitor,中间通过三个 WatchConnectivity 通道连接。安装 @capgo/capacitor-watch,使用 CapgoWatchSDK 添加 watch target,保持 payload 精简,并在提交前在配对硬件上测试。