To build an Apple Watch app for a Capacitor app, you add a native watchOS target to your iOS project, write its UI in SwiftUI, and connect it to your web code with the @capgo/capacitor-watch plugin, which wraps Apple’s WatchConnectivity framework. The phone app keeps its Capacitor web layer, and the watch app becomes a small native companion that sends and receives dictionaries.
このガイドでは、Capacitor 8 と Xcode 26 を使用した全工程を解説します。アーキテクチャ、Xcode ターゲット、各サイドの Swift および TypeScript code、最適な転送方法、シミュレーターと実機でのテスト、そして watch アプリの配信方法まで網羅しています。
なぜ watch の UI はネイティブで、iPhone は Capacitor のままなのか
watchOS には WebView が搭載されていません。そのため、watch 上には WKWebView 存在せず、Capacitor アプリを動作させることはできません。プラグインでもこの制限は回避できません。解決策は、役割を分担させることです:
| レイヤー | 実行環境 | 言語 | 役割 |
|---|---|---|---|
| Capacitor アプリが | iPhone | TypeScript、お好みのフレームワーク | ビジネスロジック、ネットワーク、認証、ストレージ |
@capgo/capacitor-watch |
iPhone | Swift (プラグイン), JS API | WatchConnectivity を JavaScript にブリッジ |
| CapgoWatchSDK | Apple Watch | Swift | Watch 上でのセッション管理 |
| Watch アプリ | Apple Watch | SwiftUI | 小画面、一目でわかる UI、クイックアクション |
実際には、Watch アプリは「依存型」です。データは iPhone から取得します。ロジックは、すでに TypeScript で書かれている iPhone 側に置いたままにし、Watch には表示に必要なものだけを送りましょう。
前提条件
- iOSプラットフォームを追加した Capacitor 8アプリ。古いバージョンを使用している場合は、 Capacitor 8 アップグレードガイド してください。
- Mac上のXcode 26を使用してください。Appleは2026年4月よりApp StoreへのアップロードにXcode 26を必須としています。詳細は Xcode 26の要件を確認してください.
- デバイスでのテストおよび配布用のApple Developerアカウント。
- iPhone側はiOS 15以降(プラグインの最小要件)、Apple Watch側はwatchOS 9以降(SDK の最小要件)が必要です。
- 理想的には、実機のiPhoneとApple Watchをペアリングして使用してください。シミュレーターも役立ちますが、すべての転送タイプを網羅しているわけではありません。
ステップ 1: iPhone側にプラグインをインストールする
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 の完全なリファレンスはこちら: プラグインのドキュメント および プラグインのページ.
ステップ 2: Xcode で watchOS ターゲットを追加する
を使用して iOS プロジェクトを開きます: bunx cap open ios. Swift Package Manager で作成された Capacitor 8 プロジェクトを開く場合 App.xcodeproj; 古い CocoaPods プロジェクトを開く場合 App.xcworkspace. どちらでも可能です。
- 選択: File > New > Targetから、以下を選択してください: watchOS タブを選択し、次に アプリ.
- 名前を入力します(例:
MyWatch)、そして SwiftUI をインターフェースとして使用します。 - オプションシートで、watchアプリが既存の
Apptargetのコンパニオンとして埋め込まれていることを確認してください。この設定により、XcodeはiPhoneのビルドにwatchアプリを組み込み、コンパニオンのbundle identifierを設定します。 - 今すぐ必要でない限り、notification scenesとcomplicationsはオフのままにしておいてください。ウィジェットは後から追加可能です。
- プロンプトが表示されたら、新しいschemeをアクティベートします。
bundle identifierのルール
WatchアプリのBundle IDは、iPhoneアプリのBundle IDから始まる必要があります。アプリが com.example.shopの場合、Xcodeは次を提案します: com.example.shop.watchkitappこのプレフィックスは維持してください。後でiOSのBundle IDを変更した場合は、Watchアプリ側も変更してください。そうしないと、ビルドのバリデーションに失敗し、WatchConnectivityによる2つのアプリのペアリングができなくなります。
未満 Signing & Capabilitiesの場合、両方のターゲットで同じチームを選択してください。Automatic signingを使用していれば、Xcodeが2つ目のApp IDとプロビジョニングプロファイルを自動的に登録します。
WatchターゲットにCapgoWatchSDKを追加する
- File > Add Package Dependencies.
- を入力
https://github.com/Cap-go/capacitor-watch.git. - Dependency Ruleは「Up to Next Major Version」を選択
8.0.0. - Xcodeで追加するプロダクトの選択を求められたら、以下のみを選択してください: CapgoWatchSDK そして、それを Apple Watch ターゲットに割り当てます(iOSアプリ側ではありません)。iOS側はすでに次の方法でプラグインを取得しています:
cap sync.
ステップ 3: SwiftUI で watch アプリを書く
SDK は次を公開します: WatchConnector.shared、つまり ObservableObject 公開された isReachable, isActivated, lastMessageや applicationContext プロパティを持つものです。アプリ起動時に有効化してください:
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 reply handler付きで送信されるため、iPhone側では messageReceivedWithReply イベントとして受信します。コールバックベースのバリアントなら sendMessage(_:replyHandler:errorHandler:) replyをスキップできます。
publishedプロパティの監視よりもdelegateコールバックを好む場合は、 WatchConnectorDelegate 設定してください WatchConnector.shared.delegateに準拠してください。メッセージ、reply handler付きメッセージ、アプリケーションコンテキスト、ユーザー情報、reachability、activationのメソッドが用意されています。
ステップ 4: TypeScriptからwatchと通信する
起動時に届くイベントを見逃さないよう、アプリのbootstrap時など早い段階でリスナーを登録してください。
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にreplyを返してください。watch側は待機しており、応答のないリクエストはwatch側でタイムアウトエラーになります。
どの転送方法を使うべきですか?
WatchConnectivityには3つのチャネルがあり、プラグインはそれぞれを以下のようにマッピングします:
| メソッド | 配信 | Watchが接続可能であること | 適した用途 |
|---|---|---|---|
sendMessage |
即時 | はい(それ以外は拒否) | ボタンタップ、両アプリ起動中のライブ更新 |
updateApplicationContext |
最新値を優先、可能な限り配信 | いいえ | 現在の状態: カウント、設定、ログインユーザー |
transferUserInfo |
キューに保存、順序通りに配信 | いいえ | 必ず届けるべきイベント:記録されたワークアウト、作成されたレコード |
推奨されるパターン:Watch側で表示する情報の信頼できるソースとしてアプリのコンテキストを使用し、 sendMessage Watchアプリが開いている間のインタラクションのみに限定して使用し、 transferUserInfo 決して妥協できないものすべてのために。
設計時に考慮すべき制限事項
- ペイロードのサイズ WatchConnectivityは、サイズが大きすぎるペイロードを
payloadTooLargeとして拒否します。Appleは各チャネルの固定バイト数を公開していません。ID、カウント、短い文字列など、ペイロードは最小限に抑えてください。大きなコンテンツは API から取得するようにしましょう。 - ペイロードの型 データはproperty-list互換である必要があります:string、number、boolean、date、data、array、dictionary。
nullJavaScriptからの値はそのままでは無効です。送信前に取り除いてください。 - イベントを処理するには、iPhone側でアプリが実行されている必要があります。 リスナーは Capacitor JavaScript ランタイム内で動作します。iOS によってアプリが終了されている場合、アプリが再起動するまでイベントは TypeScript に届きません。コンテキストやユーザー情報はシステムによってキューに保持されますが、キャッシュされたコンテキストから動作するように watch UI を設計してください。
- ペアリング Apple Watch は iPhone とのみペアリング可能です。Android ユーザーは利用できず、このプラグインの Android 実装は代わりに Wear OS を対象としています。詳細は以下の Wear OS ガイド.
- 独立性 WatchConnectivity で構築されたコンパニオンアプリは、スマートフォンに依存します。ウォッチをスマートフォンの離れた場所で使用する必要がある場合は、ウォッチのネットワーク接続を介して独自の API などのセカンドデータソースを追加してください。
シミュレータと実機でのテスト
watch シミュレータで SwiftUI のレイアウトを繰り返し調整できますが、データ転送にはペアリングされたセットアップが必要です。
xcrun simctl list devices
xcrun simctl pair <watch-udid> <phone-udid>
まずペアリングされた iPhone シミュレータで iOS アプリを実行し、次に watch シミュレータで watch スキームを実行します。両方のアプリがインストールされて初めて isWatchAppInstalled が true になります。Apple は、シミュレータが transferUserInfoをサポートしていないと述べているため、実機でそのパスを確認してください。
実機の場合は、iPhone を接続し、watch のデベロッパーモードが有効であることを確認した上で、watch スキームの実行先として watch を選択してください。 getInfo() スモークテストとして:
getInfo() 結果 |
意味 |
|---|---|
isPaired: false |
この端末にペアリングされたWatchがありません |
isWatchAppInstalled: false |
Watchアプリが未導入、またはBundle ID / Companion設定が誤っています |
isReachable: false、それ以外はtrue |
Watchアプリがフォアグラウンドにないか、通信範囲外です |
activationState: 2、すべてtrue |
準備完了: sendMessage |
トラブルシューティング
isWatchAppInstalled falseのままです。 WatchのBundle IDのプレフィックスを確認し、Watchターゲットが〜に埋め込まれているかを確認してください App ターゲット (Build Phases > Embed Watch Content) を設定し、両方のアプリを再インストールしてください。
WatchからのメッセージがJavaScriptに届きません。 Watchが送信を開始する前にリスナーを登録し、Watchからの呼び出しを確認してください。 WatchConnector.shared.activate() 起動時に確認します。iPhone側では、Xcodeのコンソールでプラグインの起動ログを確認してください。
sendMessage “Watch is not reachable” というエラーで拒否されます。 Watchアプリが閉じている場合に発生します。代わりに、 updateApplicationContext.
replyToMessage 無視されているようです。 正確な callbackId をイベントから取得し、迅速にレスポンスを返してください。Watchのreply handlerにはタイムアウトがあります。
“Embedded binary’s bundle identifier is not prefixed” というエラーでビルドが失敗します。 Watchのbundle IDは、iOSのbundle IDで始まる必要があります。
アイコンが不足しているため、Archiveのuploadが拒否されました。 Watchターゲットには、専用のアプリアイコンが Assets.xcassets.
Watchアプリのリリース
Watchアプリは独立した製品ではありません。iOSのアーカイブに組み込まれ、一緒にアップロードされ、App Store Connectの同じレコードに表示されます。WatchタブからApple Watchのスクリーンショットを追加してください。アーカイブを作成する環境(ローカルのXcode、CI、または Capgo のビルド)には、両方のbundle IDに対するプロビジョニングプロファイルが必要で、Xcode 26を使用する必要があります。
アップデートに関する注意点が1つあります。 Capgo Live Updates を使用すれば、ストア審査なしでスマホ側のweb bundleに変更を適用できます。これはWatchブリッジのTypeScript側にバグがある時に非常に便利です。ただし、Watch上のSwift codeやネイティブプラグインへの変更は、引き続きApp Storeの審査が必要です。メッセージ形式には、例えば v フィールドなどを用いてバージョン管理を行い、新しいスマホ側のbundleが古いWatchのビルドとも互換性を保てるようにしてください。
まとめ
この構成を理解すれば、仕組みはシンプルです。手首にはSwiftUI、それ以外にはCapacitor、そしてその間を繋ぐ3つのWatchConnectivityチャネル。まず @capgo/capacitor-watchをインストールし、CapgoWatchSDKでWatchターゲットを追加してください。ペイロードは小さく保ち、提出前にペアリングされた実機でテストしましょう。