Aller au contenu principal

Comment créer une app Apple Watch pour une app Capacitor

Créez une app Apple Watch pour votre app Capacitor : ajoutez une cible watchOS, développez la partie SwiftUI et synchronisez les données avec l'iPhone via @capgo/capacitor-watch.

Bannière Capgo pour un guide sur la création d'une app compagnon Apple Watch pour une app iOS Capacitor

Pour créer une app Apple Watch pour une app Capacitor, vous ajoutez une cible native watchOS à votre projet iOS, écrivez son UI en SwiftUI et la connectez à votre code web avec le @capgo/capacitor-watch plugin, qui encapsule le framework WatchConnectivity d'Apple. L'app mobile conserve sa couche web Capacitor, et l'app watch devient un petit compagnon natif qui envoie et reçoit des dictionnaires.

Ce guide couvre l'ensemble du parcours sur Capacitor 8 et Xcode 26 : l'architecture, la cible Xcode, le code Swift et TypeScript de chaque côté, la méthode de transfert à utiliser, les tests sur simulateurs et appareils, et comment l'app watch est publiée.

Pourquoi l'UI de la montre est native et le téléphone reste Capacitor

watchOS n'embarque pas de WebView. Il n'y a pas de WKWebView sur la montre, donc une app Capacitor ne peut pas y tourner, et aucun plugin ne change cela. Ce que vous pouvez faire, c'est séparer les responsabilités :

Couche S'exécute sur Langue Rôle
app Capacitor iPhone TypeScript, votre framework de choix Logique métier, réseau, auth, stockage
@capgo/capacitor-watch iPhone Swift (plugin), API JS Fait le pont entre WatchConnectivity et JavaScript
CapgoWatchSDK Apple Watch Swift Gestion des sessions sur la montre
App Watch Apple Watch SwiftUI Petits écrans, UI concise, actions rapides

En pratique, l'app Watch est « dépendante » : elle récupère ses données du téléphone. Gardez la logique sur le téléphone là où elle existe déjà en TypeScript, et n'envoyez à la montre que ce qu'elle doit afficher.

Prerequisites

  • Une application Capacitor 8 avec la plateforme iOS ajoutée. Si vous utilisez encore une version antérieure, suivez le Capacitor guide de mise à niveau d'abord.
  • Xcode 26 sur un Mac. Apple impose Xcode 26 pour les uploads sur l’App Store depuis avril 2026, voir l’exigence Xcode 26.
  • Un compte Apple Developer pour les tests sur appareil et la distribution.
  • iOS 15 ou version ultérieure sur le téléphone (minimum du plugin) et watchOS 9 ou version ultérieure sur la montre (minimum SDK).
  • Idéalement, un vrai iPhone appairé avec une vraie Apple Watch. Les simulateurs aident, mais ils ne couvrent pas tous les types de transfert.

Étape 1 : Installer le plugin côté téléphone

bun add @capgo/capacitor-watch
bunx cap sync ios

Le plugin s’active WCSession dès son chargement, il n’y a donc aucun appel d’init. 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 sur la doc du plugin et le page du plugin.

Étape 2 : ajouter une cible watchOS dans Xcode

Ouvrez le projet iOS avec bunx cap open ios. Capacitor ouvre les projets créés avec Swift Package Manager App.xcodeproj; les anciens projets CocoaPods s'ouvrent App.xcworkspace. Les deux fonctionnent.

  1. Choisissez File > New > Target, sélectionnez la watchOS tab, puis cible.
  2. Nommez-le, par exemple MyWatch, et utilisez SwiftUI comme interface.
  3. Dans la feuille d'options, assurez-vous que l'app watch est intégrée à votre App cible existante en tant qu'application compagnon. C'est ce réglage qui permet à Xcode d'intégrer l'app watch dans le build iPhone et de définir l'identifiant de bundle compagnon.
  4. Leave notification scenes and complications off unless you need them now. You can add widgets later.
  5. Acceptez le prompt pour activer le nouveau schéma.

La règle de l’identifiant de bundle

L’identifiant de bundle de l’app Watch doit commencer par l’identifiant de bundle de l’app iPhone. Si votre app est com.example.shop, Xcode propose com.example.shop.watchkitapp. Conservez ce préfixe. Si vous modifiez l’identifiant de bundle iOS plus tard, modifiez aussi celui de Watch, sinon le build échouera la validation et WatchConnectivity n’associera pas les deux apps.

Inférieur à Signature et capacités, sélectionnez la même équipe pour les deux cibles. Avec la signature automatique, Xcode enregistre le second App ID et le profil de provisionnement pour vous.

Ajouter CapgoWatchSDK à la cible watch

  1. Fichier > Ajouter des dépendances de package.
  2. Saisissez https://github.com/Cap-go/capacitor-watch.git.
  3. Utiliser « Up to Next Major Version » à partir 8.0.0.
  4. Lorsque Xcode demande quels produits ajouter, sélectionnez uniquement CapgoWatchSDK et l'assigner à la cible Surveiller , pas à l'app iOS. Le côté iOS reçoit déjà le plugin via cap sync.

Étape 3 : écrire l'app watch en SwiftUI

Le SDK expose WatchConnector.shared, un ObservableObject avec les versions publiées isReachable, isActivated, lastMessage, et applicationContext propriétés publiées. Activez-l’au démarrage de l'app :

import SwiftUI
import CapgoWatchSDK

@main
struct MyWatchApp: App {
    init() {
        WatchConnector.shared.activate()
    }

    var body: some Scene {
        WindowGroup {
            OrdersView()
        }
    }
}

Un petit écran qui affiche les données envoyées par le téléphone et renvoie une action :

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, pour que le téléphone le reçoive comme un messageReceivedWithReply événement. La variante basée sur des callbacks sendMessage(_:replyHandler:errorHandler:) vous permet de ne pas répondre.

Si vous préférez les callbacks de délégué à l'observation des propriétés publiées, conformez-vous à WatchConnectorDelegate et configurez WatchConnector.shared.delegate. Il dispose de méthodes pour les messages, les messages avec un gestionnaire de réponse, le contexte d'application, les informations utilisateur, la joignabilité et l'activation.

Étape 4 : Communiquer avec la montre depuis TypeScript

Enregistrez les listeners tôt, par exemple dans le bootstrap de votre app, pour ne pas manquer les événements arrivant 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. La montre attend, et une requête sans réponse entraîne une erreur de timeout côté montre.

Quelle méthode de transfert utiliser ?

WatchConnectivity propose trois canaux, et le plugin mappe chacun d'eux :

Méthode Livraison La montre doit être joignable Idéal pour
sendMessage Immédiat Oui, sinon rejeté Appuis sur les boutons, live updates quand les deux apps sont ouvertes
updateApplicationContext La valeur la plus récente l'emporte, livrée dès que possible Non État actuel : compteurs, paramètres, utilisateur connecté
transferUserInfo En file d'attente, livrés dans l'ordre Non Événements qui doivent tous arriver : entraînements enregistrés, fiches créées

Un schéma qui fonctionne bien : utiliser le contexte de l’application comme source de vérité pour ce que montre la montre, utiliser sendMessage uniquement pour les interactions lorsque l’application Watch est ouverte, et utilisez transferUserInfo for anything you cannot afford to drop.

Limites à prendre en compte

  • Taille du payload. WatchConnectivity rejette les payloads trop volumineux avec payloadTooLarge, et Apple ne publie pas de nombre de bytes fixe pour chaque canal. Gardez les payloads petits : IDs, compteurs, chaînes courtes. Récupérez le contenu volumineux depuis votre API.
  • Types de payload. Les données doivent être compatibles avec les property lists : chaînes, nombres, booléens, dates, données, tableaux et dictionnaires. null Les valeurs issues de JavaScript ne sont pas valides. Supprimez-les avant l’envoi.
  • Votre application doit tourner sur le téléphone pour traiter les événements. Les listeners résident dans le runtime JavaScript Capacitor. Si iOS a tué l'app, les événements n'atteindront votre TypeScript que lorsque l'app sera de nouveau en cours d'exécution. Le contexte et les infos utilisateur restent en file d'attente côté système, mais concevez l'UI de la montre pour qu'elle fonctionne à partir du contexte en cache.
  • Appairage. Une Apple Watch ne s'appaire qu'avec un iPhone. Les utilisateurs Android ne peuvent pas l'utiliser, et l'implémentation Android du plugin vise plutôt Wear OS, comme expliqué dans notre guide Wear OS.
  • Indépendance. Une application compagnon basée sur WatchConnectivity dépend du téléphone. Si la montre doit fonctionner loin du téléphone, ajoutez une seconde source de données comme votre propre API via la connexion réseau de la montre.

Tests sur simulateurs et appareils

Vous pouvez itérer sur la mise en page SwiftUI dans le simulateur de montre, mais le transfert de données nécessite une configuration appairée.

xcrun simctl list devices
xcrun simctl pair <watch-udid> <phone-udid>

Lancez l'app iOS sur le simulateur iPhone appairé, puis lancez le scheme de la montre sur le simulateur de montre. Les deux apps doivent être installées avant que isWatchAppInstalled devient vrai. Apple précise que le Simulator ne prend pas en charge transferUserInfo, donc vérifiez ce chemin sur du matériel réel.

Pour les appareils, branchez l'iPhone, assurez-vous que le mode développeur est activé sur la montre, et sélectionnez la montre comme destination d'exécution pour le scheme de la montre. Utilisez getInfo() comme smoke test :

getInfo() résultat Signification
isPaired: false Aucune montre associée à ce téléphone
isWatchAppInstalled: false App Watch manquante, ou bundle ID / paramètre companion incorrect
isReachable: false, le reste est vrai L'app Watch n'est pas au premier plan ou est hors de portée
activationState: 2, tout est vrai Prêt pour sendMessage

Troubleshooting

isWatchAppInstalled reste à false. Vérifiez le préfixe du bundle ID de la montre, vérifiez que la cible watch est intégrée dans la App cible (Build Phases > Embed Watch Content) et réinstallez les deux apps.

Les messages de la montre n’atteignent jamais JavaScript. Enregistrez les listeners avant que la montre n’envoie, et confirmez que la montre appelle WatchConnector.shared.activate() au lancement. Sur le téléphone, vérifiez le log d’activation du plugin dans la console Xcode.

sendMessage rejette avec « Watch is not reachable ». Attendu lorsque l’app de la montre est fermée. Basculez vers updateApplicationContext.

replyToMessage semble ignoré. Pass the exact callbackId exact de l’événement et répondez rapidement. Le handler de réponse de la montre a un timeout.

Le build échoue avec l’erreur « Embedded binary’s bundle identifier is not prefixed ». The watch bundle ID must start with the iOS bundle ID.

L’upload de l’archive est refusé en raison d’icônes manquantes. Watch targets need their own app icon in Assets.xcassets.

Publier l’application Watch

L’application Watch n’est pas un produit distinct. Elle est intégrée à l’archive iOS, envoyée avec elle, et affichée sur le même enregistrement App Store Connect, où vous ajoutez les captures d’écran Apple Watch dans l’onglet Watch. Tout ce qui produit votre archive, Xcode en local, la CI ou Build Capgo, nécessite un profil de provisionnement pour les deux identifiants de bundle et doit utiliser Xcode 26.

Gardez une chose en tête pour les mises à jour. les live updates Capgo peut déployer des modifications du bundle web du téléphone sans passage en revue par le store, ce qui est pratique lorsque le côté TypeScript de votre pont watch contient un bug. Le Swift code sur la Watch, et toute modification du plugin natif, passent toujours par l’App Store. Versionnez votre format de message, par exemple avec un v champ dans chaque dictionnaire, afin qu’un bundle téléphone plus récent continue de fonctionner avec un build Watch plus ancien.

Récapitulatif

La répartition est simple une fois qu’on l’accepte : SwiftUI pour le poignet, Capacitor pour le reste, et trois canaux WatchConnectivity entre les deux. Installer @capgo/capacitor-watch, ajouter la cible watch avec CapgoWatchSDK, garder des payloads légers et tester sur du matériel appairé avant de publier.

Live updates pour les apps Capacitor

Lorsqu'un bug de la couche web est en ligne, déployez le correctif via Capgo au lieu d'attendre des jours l'approbation des stores. Les utilisateurs reçoivent la mise à jour en arrière-plan, tandis que les changements natifs suivent le circuit de validation habituel.

Les derniers articles du blog

Capgo vous offre les meilleures informations pour créer une application mobile vraiment professionnelle.