Um eine Apple-Watch-App für eine Capacitor-App zu erstellen, fügst du ein natives watchOS-Target zu deinem iOS-Projekt hinzu, entwickelst die UI in SwiftUI und verbindest sie mit deinem Web-code über das @capgo/capacitor-watch Plugin, das Apples WatchConnectivity-Framework kapselt. Die Handy-App behält ihre Capacitor-Web-Schicht bei, während die Watch-App eine kleine native Begleit-App wird, die Wörterbücher sendet und empfängt.
Dieser Leitfaden zeigt den kompletten Weg mit Capacitor 8 und Xcode 26: die Architektur, das Xcode-Target, den Swift- und TypeScript-code auf beiden Seiten, welche Übertragungsmethode du verwendest, Tests auf Simulatoren und Geräten und wie die Watch-App ausgeliefert wird.
Warum die Watch-UI nativ ist und das Phone Capacitor bleibt
watchOS hat keinen WebView. Es gibt keinen WKWebView auf der Watch, daher kann eine Capacitor-App dort nicht laufen, und kein Plugin ändert etwas daran. Was du tun kannst, ist die Verantwortlichkeiten aufzuteilen:
| Schicht | Läuft auf | Sprache | Aufgabe |
|---|---|---|---|
| Capacitor App | iPhone | TypeScript, dein Framework der Wahl | Business-Logik, Netzwerk, Auth, Storage |
@capgo/capacitor-watch |
iPhone | Swift (Plugin), JS API | Verbindet WatchConnectivity mit JavaScript |
| CapgoWatchSDK | Apple Watch | Swift | Session-Handling auf der Watch |
| Watch-App | Apple Watch | SwiftUI | Kleine Displays, auf einen Blick, schnelle Aktionen |
In der Praxis ist die Watch-App „abhängig“: Sie erhält ihre Daten vom Telefon. Halte die Logik auf dem Telefon, wo sie bereits in TypeScript existiert, und sende der Watch nur das, was sie zur Anzeige benötigt.
Voraussetzungen
- Eine Capacitor 8-App mit der iOS-Plattform. Falls du noch eine ältere Version nutzt, folge Capacitor 8 Upgrade-Guide zuerst.
- Xcode 26 auf einem Mac. Apple verlangt seit April 2026 Xcode 26 für App-Store-Uploads, siehe die Xcode-26-Anforderung.
- Ein Apple-Developer-Konto für Gerätetests und Distribution.
- iOS 15 oder neuer auf dem Telefon (Minimum des Plugins) und watchOS 9 oder neuer auf der Watch (Minimum von SDK).
- Idealerweise ein echtes iPhone, gekoppelt mit einer echten Apple Watch. Simulatoren helfen, decken aber nicht jeden Übertragungstyp ab.
Schritt 1: Plugin auf der Smartphone-Seite installieren
bun add @capgo/capacitor-watch
bunx cap sync ios
Das Plugin aktiviert WCSession sich sofort beim Laden, es gibt keinen init-Aufruf. Beginne damit, den Verbindungsstatus zu lesen:
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
});
Die vollständige API-Referenz findest du in der Plugin-Docs und Plugin-Seite.
Schritt 2: watchOS-Target in Xcode hinzufügen
Öffne das iOS-Projekt mit bunx cap open ios. Capacitor 8-Projekte, die mit Swift Package Manager erstellt wurden, öffnen App.xcodeproj; ältere CocoaPods-Projekte öffnen App.xcworkspace. Beides funktioniert.
- Wähle Datei > Neu > Ziel, wählen Sie watchOS Tab, dann App hinzu.
- Gib einen Namen ein, z. B.
MyWatch, und verwende SwiftUI als Interface. - Stelle im Options-Sheet sicher, dass die Watch-App als Companion in dein bestehendes
AppTarget eingebettet wird. Diese Einstellung sorgt dafür, dass Xcode die Watch-App in den iPhone-Build einbettet und den Companion Bundle Identifier setzt. - Lass Notification Scenes und Complications deaktiviert, sofern du sie nicht sofort brauchst. Widgets kannst du später hinzufügen.
- Bestätige den Prompt, um das neue Scheme zu aktivieren.
Die Bundle-Identifier-Regel
Die Bundle-ID der Watch-App muss mit der Bundle-ID der iPhone-App beginnen. Wenn deine App com.example.shopist, schlägt Xcode com.example.shop.watchkitappvor. Behalte dieses Präfix bei. Wenn du die iOS-Bundle-ID später änderst, ändere auch die der Watch-App, sonst schlägt die Build-Validierung fehl und WatchConnectivity kann die beiden Apps nicht koppeln.
Unter Signing & Capabilities, wähle für beide Targets dasselbe Team. Bei automatischer Signierung registriert Xcode die zweite App-ID und das Provisioning-Profil für dich.
CapgoWatchSDK zum Watch-Target hinzufügen
- Datei > Paketabhängigkeiten hinzufügen.
- Eingabe
https://github.com/Cap-go/capacitor-watch.git. - Verwenden Sie „Bis zur nächsten Major-Version“ ab
8.0.0. - Wenn Xcode fragt, welche Produkte hinzugefügt werden sollen, wählen Sie nur CapgoWatchSDK aus und weisen Sie es dem Watch- Target zu, nicht dem iOS-App-Target. Die iOS-Seite erhält das Plugin bereits über
cap sync.
Schritt 3: Die Watch-App in SwiftUI schreiben
Das SDK stellt WatchConnector.shared, ein ObservableObject mit veröffentlichten isReachable, isActivated, lastMessage, und applicationContext Properties bereit. Aktivieren Sie es beim App-Start:
import SwiftUI
import CapgoWatchSDK
@main
struct MyWatchApp: App {
init() {
WatchConnector.shared.activate()
}
var body: some Scene {
WindowGroup {
OrdersView()
}
}
}
Ein kleines Display, das vom Telefon übermittelte Daten anzeigt und eine Aktion zurücksendet:
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"
}
}
}
}
Der async Variante von sendMessage sendet mit einem Reply-Handler, sodass das Telefon es als messageReceivedWithReply Event erhält. Die callback-basierte Variante sendMessage(_:replyHandler:errorHandler:) ermöglicht es, auf die Antwort zu verzichten.
Wenn Sie Delegate-Callbacks dem Beobachten veröffentlichter Eigenschaften vorziehen, implementieren Sie WatchConnectorDelegate und setze WatchConnector.shared.delegate. Es bietet Methoden für Nachrichten, Nachrichten mit Reply-Handler, Anwendungskontext, Benutzerinformationen, Erreichbarkeit und Aktivierung.
Schritt 4: Kommunikation mit der Watch aus TypeScript
Registrieren Sie Listener frühzeitig, z. B. im App-Bootstrap, damit Events, die beim Start eintreffen, nicht verloren gehen.
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() },
});
}
Antworten Sie immer auf messageReceivedWithReply. Die Watch wartet, und eine unbeantwortete Anfrage führt zu einem Timeout-Fehler auf der Watch-Seite.
Welche Übertragungsmethode solltest du verwenden?
WatchConnectivity bietet drei Kanäle, und das Plugin mappt jeden davon:
| Methode | Auslieferung | Watch muss erreichbar sein | Ideal für |
|---|---|---|---|
sendMessage |
Sofort | Ja, sonst wird abgelehnt | Button-Taps, Live Updates, solange beide Apps offen sind |
updateApplicationContext |
Neuester Wert gewinnt, Zustellung sobald möglich | Nein | Aktueller Status: Zähler, Einstellungen, eingeloggter User |
transferUserInfo |
In Warteschlange, Zustellung in Reihenfolge | Nein | Events, die sicher ankommen müssen: geloggte Workouts, erstellte Datensätze |
Ein bewährtes Muster: Nutze den Application Context als Source of Truth dafür, was die Watch anzeigt, nutze sendMessage nur für Interaktionen, während die Watch-App offen ist, und nutze transferUserInfo für alles, was nicht verloren gehen darf.
Design-Einschränkungen
- Payload-Größe. WatchConnectivity lehnt zu große Payloads mit
payloadTooLargeab, und Apple gibt keine feste Byte-Anzahl pro Channel an. Halte Payloads klein: IDs, Anzahlen, kurze Strings. Hol dir große Inhalte stattdessen aus deinem API. - Payload-Typen. Daten müssen Property-List-kompatibel sein: Strings, Zahlen, Booleans, Datumswerte, Data, Arrays und Dictionaries.
nullWerte aus JavaScript sind ungültig. Entferne sie vor dem Senden. - Die App muss auf dem iPhone laufen, um Events zu verarbeiten. Listener laufen in der Capacitor JavaScript-Runtime. Wenn iOS die App beendet hat, erreichen Events dein TypeScript erst, wenn die App wieder läuft. Kontext und Nutzerinfos bleiben vom System in der Warteschlange; gestalte die Watch-UI aber so, dass sie mit gecachtem Kontext funktioniert.
- Pairing. Eine Apple Watch lässt sich nur mit einem iPhone koppeln. Android-Nutzer können sie nicht nutzen; die Android-Implementierung des Plugins zielt stattdessen auf Wear OS ab – mehr dazu in unserem Wear OS Guide.
- Unabhängigkeit. Eine Companion-App auf WatchConnectivity-Basis ist vom iPhone abhängig. Wenn die Watch weit weg vom iPhone funktionieren muss, füge eine zweite Datenquelle hinzu, etwa dein eigenes API über die Netzwerkverbindung der Watch.
Testen auf Simulatoren und Geräten
Du kannst am SwiftUI-Layout im Watch-Simulator iterieren, aber für den Datentransfer brauchst du ein gekoppeltes Setup.
xcrun simctl list devices
xcrun simctl pair <watch-udid> <phone-udid>
Starte die iOS-App auf dem gekoppelten iPhone-Simulator und führe anschließend das Watch-Scheme auf dem Watch-Simulator aus. Beide Apps müssen vorab installiert sein. isWatchAppInstalled true wird. Apple weist darauf hin, dass der Simulator transferUserInfo, also diesen Pfad auf echtem Hardware-Setup prüfen.
Schließe für Geräte das iPhone an, stelle sicher, dass auf der Watch der Developer Mode aktiviert ist, und wähle die Watch als Run Destination für das Watch-Scheme aus. Nutze getInfo() als Smoke Test:
getInfo() Ergebnis |
Bedeutung |
|---|---|
isPaired: false |
Keine Watch mit diesem Telefon gekoppelt |
isWatchAppInstalled: false |
Watch-App fehlt oder Bundle-ID / Companion-Einstellung falsch |
isReachable: false, Rest true |
Watch-App nicht im Vordergrund oder außerhalb des Bereichs |
activationState: 2, alle true |
Bereit für sendMessage |
Fehlerbehebung
isWatchAppInstalled bleibt false. Prüfe das Präfix der Watch-Bundle-ID und stelle sicher, dass das Watch-Target eingebettet ist App Target eingebettet ist (Build Phases > Embed Watch Content), und installiere beide Apps neu.
Nachrichten von der Watch kommen nie in JavaScript an. Registriere Listener, bevor die Watch sendet, und stelle sicher, dass die Watch WatchConnector.shared.activate() beim Start aufruft. Prüfe auf dem Telefon in der Xcode-Konsole den Aktivierungs-Log des Plugins.
sendMessage wird mit „Watch is not reachable“ abgelehnt. Erwartet, wenn die Watch-App geschlossen ist. Fallback auf updateApplicationContext.
replyToMessage scheint ignoriert zu werden. Übergib genau das callbackId aus dem Event und antworte schnell. Der Reply-Handler der Watch hat ein Timeout.
Build schlägt fehl mit „Embedded binary’s bundle identifier is not prefixed“. Die Watch-Bundle-ID muss mit der iOS-Bundle-ID beginnen.
Archive-Upload abgelehnt wegen fehlender Icons. Watch-Targets benötigen ihr eigenes App-Icon in Assets.xcassets.
Veröffentlichung der Watch-App
Die Watch-App ist kein separates Produkt. Sie ist im iOS-Archiv eingebettet, wird zusammen hochgeladen und erscheint im selben App Store Connect-Eintrag, wo du unter dem Watch-Tab Apple Watch-Screenshots hinzufügst. Egal, was dein Archiv erzeugt – lokales Xcode, CI oder Capgo Build, benötigt ein Provisioning-Profil für beide Bundle-IDs und muss Xcode 26 verwenden.
Beachte bei Updates Folgendes. Capgo Live Updates kann Änderungen am Web-Bundle des Telefons ohne Store-Review veröffentlichen, was praktisch ist, wenn die TypeScript-Seite deiner Watch-Bridge einen Bug hat. Swift code auf der Watch und jede Änderung am nativen Plugin gehen weiterhin über den App Store. Versioniere dein Nachrichtenformat, zum Beispiel mit einem v Feld in jedem Dictionary, damit ein neueres Phone-Bundle mit einem älteren Watch-Build zusammenarbeitet.
Fazit
Die Aufteilung ist einfach, wenn man sie akzeptiert: SwiftUI für das Handgelenk, Capacitor für alles andere und drei WatchConnectivity-Kanäle dazwischen. Installieren @capgo/capacitor-watch, füge das Watch-Target mit CapgoWatchSDK hinzu, halte die Payloads klein und teste auf gepaarter Hardware, bevor du einreichst.