Pour créer une application Apple Watch pour une application Capacitor, vous ajoutez une cible native watchOS à votre projet iOS, écrivez son interface utilisateur en SwiftUI et la connectez à votre web code avec le @capgo/capacitor-watch plugin, qui enveloppe le cadre de travail WatchConnectivity d'Apple. L'application de téléphone conserve son layer web Capacitor, et l'application de montre devient un petit compagnon natif qui envoie et reçoit des dictionnaires.
This guide covers the full path on Capacitor 8 and Xcode 26: the architecture, the Xcode target, the Swift and TypeScript code on each side, which transfer method to use, testing on simulators and devices, and how the watch app ships.
Pourquoi l'interface de l'horloge est native et le téléphone reste Capacitor
watchOS ne fournit pas de WebView. Il n'y a WKWebView sur la montre, donc une application Capacitor ne peut pas s'exécuter là, et aucun plugin ne peut changer cela. Vous pouvez cependant diviser les responsabilités :
| Couche | Exécute sur | Langue | Poste |
|---|---|---|---|
| Capacitor application | Iphone | TypeScript, votre framework de choix | Logique métier, réseau, authentification, stockage |
@capgo/capacitor-watch |
Iphone | Swift (plugin), JS API | Permet l'accès à WatchConnectivity en JavaScript |
| CapgoWatchSDK | Montre Apple | Swift | Gestion de session sur l'horloge |
| Watch app | Montre Apple | SwiftUI | Écrans petits, interface de vue rapide, actions rapides |
En pratique, l'application horloge est « dépendante » : elle obtient ses données du téléphone. Conservez la logique sur le téléphone où elle existe déjà en TypeScript, et envoyez à l'horloge uniquement ce dont elle a besoin pour afficher.
Prerequisites
- A Capacitor 8 app with the iOS platform added. If you are still on an older version, follow the Capacitor 8 upgrade guide Premièrement.
- Xcode 26 sur un Mac. Apple a exigé Xcode 26 pour les téléchargements de l'App Store depuis avril 2026, voir les exigences Xcode 26.
- Un compte développeur Apple pour les tests et la distribution sur appareil.
- iOS 15 ou ultérieur sur le téléphone (la version minimale du plugin) et watchOS 9 ou ultérieur sur la montre (la version minimale de SDK).
- Un iPhone réel associé à une montre Apple réelle. Les simulateurs sont utiles, mais ils ne couvrent pas tous les types de transfert.
Étape 1 : Installez le plugin sur le côté téléphone
bun add @capgo/capacitor-watch
bunx cap sync ios
Le plugin est activé WCSession à mesure qu'il charge, il n'y a donc pas d'appel d'initialisation. Commencez par lire l'état de la connexion :
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
});
La référence complète de API se trouve dans les docs du plugin et la page du plugin.
Étape 2 : Ajoutez un cible watchOS dans Xcode
Ouvrez le projet iOS avec bunx cap open ios. Capacitor 8 projets créés avec Swift Package Manager ouverts App.xcodeproj; les anciens projets CocoaPods ouverts App.xcworkspace. N'importe laquelle fonctionne.
- Choisissez Fichier > Nouveau > Cible, sélectionnez l'onglet watchOS , puis Application.
- Nommez-le, par exemple
MyWatch, et utilisez SwiftUI comme l'interface. - Dans la feuille d'options, assurez-vous que l'application pour montre soit intégrée à votre projet existant.
AppFixer comme son compagnon. C'est ce paramètre qui fait que Xcode intègre l'application montre dans la build iPhone et défini l'identifiant de la bibliothèque du compagnon. - N'activez les scènes de notification et les complications que vous n'utilisez pas immédiatement. Vous pouvez ajouter des widgets plus tard.
- Acceptez la prompte pour activer le nouveau schéma.
La règle d'identifiant de bundle
L’ID de bundle de l'application horlogère doit commencer par l'ID de bundle de l'application iPhone. Si votre application est com.example.shop, Xcode propose com.example.shop.watchkitapp. Keep that prefix. If you change the iOS bundle ID later, change the watch one too, or the build will fail validation and WatchConnectivity will not pair the two apps.
Sous Sécurité et capacitésSélectionnez le même équipe pour les deux cibles. Avec l'inscription automatique, Xcode enregistre le deuxième ID d'application et le profil de provisioning pour vous.
Ajoutez CapgoWatchSDK à la cible de montre
- Dossiers > Ajouter les dépendances de package.
- Entrer
https://github.com/Cap-go/capacitor-watch.git. - Utilisez “Jusqu'à la prochaine version majeure”
8.0.0. - Quand Xcode demande quelles produits ajouter, sélectionnez uniquement CapgoWatchSDK et affectez-l’à la montre target, pas l'application iOS. La partie iOS reçoit déjà le plugin par le biais de
cap sync.
Étape 3 : Écrivez l'application de montre en SwiftUI
Le SDK expose WatchConnector.shared, un ObservableObject avec des publications isReachable, isActivated, lastMessage, et applicationContext les propriétés. Activez-le lorsque l'application démarre :
import SwiftUI
import CapgoWatchSDK
@main
struct MyWatchApp: App {
init() {
WatchConnector.shared.activate()
}
var body: some Scene {
WindowGroup {
OrdersView()
}
}
}
Une petite écran qui affiche des données envoyées depuis le téléphone et envoie une action de retour :
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"
}
}
}
}
Le async variante de sendMessage envoie avec un gestionnaire de réponse, afin que le téléphone le reçoive comme un messageReceivedWithReply la variante basée sur l'appel de retour sendMessage(_:replyHandler:errorHandler:) vous permet de passer sous silence la réponse.
Si vous préférez les appels de rappel de délégué plutôt que d'observer les propriétés publiées, conformez-vous à WatchConnectorDelegate et définissez WatchConnector.shared.delegateIl propose des méthodes pour les messages, les messages avec un gestionnaire de réponse, le contexte d'application, les informations utilisateur, la connectivité et l'activation.
Étape 4 : Parlez à l'horloge à partir de TypeScript
Enregistrez les écouteurs tôt, par exemple dans votre bootstrap d'application, afin d'éviter de manquer les événements qui arrivent au lancement.
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() },
});
}
Répondez toujours à messageReceivedWithReply. L'horloge attend, et une demande non répondu se termine par une erreur de temps limite du côté de l'horloge.
Quel méthode de transfert devriez-vous utiliser ?
WatchConnectivity vous donne trois canaux, et le plugin cartographie chacun d'eux :
| Méthode | Livraison | Besoin de l'horloge accessible | Bon pour |
|---|---|---|---|
sendMessage |
Immédiat | Oui, rejette sinon | Button taps, live updates while both apps are open |
updateApplicationContext |
La valeur la plus récente l'emporte, livrée lorsque possible | Non | État actuel : comptes, paramètres, utilisateur connecté |
transferUserInfo |
En file d'attente, livré dans l'ordre | Non | Événements qui doivent tous arriver : entraînements enregistrés, enregistrements créés |
Un modèle qui fonctionne bien : utilisez le contexte de l'application comme source de vérité pour ce que l'horloge affiche, utilisez sendMessage only for interactions while the watch app is open, and use transferUserInfo for anything you cannot afford to drop.
Limites à prendre en compte
- Taille du payload. WatchConnectivity rejette les payloads qui sont trop grands avec
payloadTooLarge, et Apple ne publie pas un comptage de bytes fixe pour chaque canal. Gardez les payloads petites : IDs, comptes, chaînes de caractères courtes. Récupérez du contenu volumineux à partir de votre API au lieu de cela. - Types de payloads. Les données doivent être compatibles avec la liste de propriétés : chaînes, nombres, booléens, dates, données, tableaux et dictionnaires.
nullLes valeurs provenant de JavaScript ne sont pas valides. Supprimez-les avant d'envoyer. - Le téléphone doit exécuter votre application pour gérer les événements. Listeners live in the Capacitor JavaScript runtime. If iOS has terminated the app, events will not reach your TypeScript until the app runs again. Context and user info stay queued by the system, but design the watch UI so it works from cached context.
- Branchement. Une montre Apple ne se branche qu'à un iPhone. Les utilisateurs Android ne peuvent pas l'utiliser, et l'implémentation Android du plugin vise Wear OS à la place, couverte dans notre Guide de Wear OS.
- Indépendance. Ainsi, une application de compagnie construite sur WatchConnectivity dépend de l'appareil mobile. Si l'horloge doit fonctionner loin de l'appareil mobile, ajoutez une deuxième source de données, comme votre propre API via la connexion réseau de l'horloge.
Testez sur les simulateurs et les appareils
Vous pouvez itérer sur la disposition SwiftUI dans le simulateur d'horloge, mais le transfert de données nécessite un ensemble associé.
xcrun simctl list devices
xcrun simctl pair <watch-udid> <phone-udid>
Démarrez l'application iOS sur le simulateur d'iPhone pairé, puis démarrez le schéma de l'horloge sur le simulateur d'horloge. Les deux applications doivent être installées avant isWatchAppInstalled devient vrai. Apple note que le simulateur ne prend pas en charge transferUserInfo, donc confirmez ce chemin sur un matériel réel.
Pour les appareils, connectez l'iPhone, assurez-vous que l'horloge a le mode développeur activé, et sélectionnez l'horloge comme destination de lancement pour le schéma de l'horloge. Utilisez getInfo() comme votre test de fumée :
getInfo() résultat |
Signification |
|---|---|
isPaired: false |
Aucune horloge pairée avec cet appareil mobile |
isWatchAppInstalled: false |
L'application horloge manquante, ou ID de bundle / paramètres de compagnie incorrects |
isReachable: false, vrai par défaut |
L'application d'horloge n'est pas en avant-plan ou hors de portée |
activationState: 2, vrai pour tous |
Prêt pour sendMessage |
Troubleshooting
isWatchAppInstalled Résultat reste faux. Vérifiez le préfixe d'ID de l'application d'horloge, assurez-vous que la cible d'horloge est intégrée dans la App cible (Build Phases > Intégrer le contenu de l'horloge), et réinstallez les deux applications.
Les messages de l'horloge ne parviennent jamais au JavaScript. Inscrivez-vous aux écouteurs avant que l'horloge n'envoie, et confirmez que l'horloge appelle WatchConnector.shared.activate() sur lancement. Sur le téléphone, vérifiez la console Xcode pour le journal d'activation du plugin.
sendMessage est rejeté avec « L'horloge n'est pas accessible ». Attendu lorsque l'application horlogère est fermée. Revenir à updateApplicationContext.
replyToMessage se semble ignoré. Passer l'exactitude callbackId du événement et répondre rapidement. Le gestionnaire de réponse de l'horloge a un temps limite.
La construction échoue avec « L'identifiant de bundle du binôme intégré n'est pas préfixé ». L’ID de l'application Apple Watch doit commencer par l'ID de l'application iOS.
L'envoi de l'archive rejeté pour manque d'icônes. L'horloge nécessite son propre icône d'application dans Assets.xcassets.
L'expédition de l'application horlogère
L'application horlogère n'est pas un produit séparé. Elle est intégrée dans l'archive iOS, envoyée ensemble et affichée sur le même registre App Store Connect, où vous ajoutez les captures d'écran Apple Watch sous l'onglet horloge. Quoi que produise votre archive, Xcode local, CI ou Capgo Construction, nécessite un profil de provisionnement pour les deux identifiants de bundle et doit utiliser Xcode 26.
Prenez en compte une chose pour les mises à jour. Capgo mises à jour en direct peuvent envoyer des modifications au bundle web du téléphone sans passer par une revue de l'App Store, ce qui est utile lorsque le côté TypeScript de votre pont d'horloge a un bug. Swift code sur l'horloge, et toute modification du plugin natif, passe toujours par l'App Store. Versionnez votre format de message, par exemple avec un v champ dans chaque dictionnaire, donc un nouveau téléphone est compatible avec une ancienne version de l'horloge.
Résumé
La séparation est simple dès que vous l'acceptez : SwiftUI pour le poignet, Capacitor pour tout le reste, et trois canaux de WatchConnectivity entre les deux. Installez @capgo/capacitor-watchajoutez la cible de l'horloge avec CapgoWatchSDK, gardez les payloads petits, et testez sur matériel pairé avant de soumettre.