Saltar al contenido principal
Tutorial iOS Capacitor

Cómo crear una app para Apple Watch para una app de Capacitor

Crea una app para Apple Watch para tu app de Capacitor: añade un objetivo watchOS, escribe la parte de SwiftUI y sincroniza datos con el iPhone usando @capgo/capacitor-watch.

Banner de Capgo para una guía sobre cómo crear una app de Apple Watch complementaria para una app iOS de Capacitor

Para crear una app de Apple Watch para una app de Capacitor, debes añadir un objetivo nativo de watchOS a tu proyecto iOS, escribir su interfaz en SwiftUI y conectarla con tu capa web de code mediante el @capgo/capacitor-watch plugin, que envuelve el framework WatchConnectivity de Apple. La app del teléfono mantiene su capa web de Capacitor, y la app del reloj se convierte en un pequeño complemento nativo que envía y recibe diccionarios.

Esta guía cubre todo el proceso en Capacitor 8 y Xcode 26: la arquitectura, el target de Xcode, el code de Swift y TypeScript en cada lado, qué método de transferencia usar, pruebas en simuladores y dispositivos, y cómo se distribuye la watch app.

Por qué la UI del watch es nativa y el teléfono sigue siendo Capacitor

watchOS no incluye un WebView. No hay WKWebView en el watch, por lo que una app Capacitor no puede ejecutarse allí, y ningún plugin lo cambia. Lo que puedes hacer es dividir responsabilidades:

Capa Se ejecuta en Idioma Tarea
App Capacitor iPhone TypeScript, el framework que prefieras Lógica de negocio, red, auth, storage
@capgo/capacitor-watch iPhone Swift (plugin), JS API Conecta WatchConnectivity con JavaScript
CapgoWatchSDK Apple Watch Swift Gestión de sesiones en el Apple Watch
App de Watch Apple Watch SwiftUI Pantallas pequeñas, UI de un vistazo y acciones rápidas

En la práctica, la app del Apple Watch es “dependiente”: recibe los datos del teléfono. Mantén la lógica en el teléfono, donde ya existe en TypeScript, y envía al Apple Watch solo lo que necesita para mostrar.

Requisitos

  • Una app de Capacitor 8 con la plataforma iOS añadida. Si aún usas una versión anterior, sigue el guía de actualización de Capacitor 8 primero.
  • Xcode 26 en un Mac. Apple exige Xcode 26 para las subidas a App Store desde abril de 2026, ver el requisito de Xcode 26.
  • Una cuenta de Apple Developer para pruebas en dispositivos y distribución.
  • iOS 15 o posterior en el teléfono (mínimo del plugin) y watchOS 9 o posterior en el reloj (mínimo de SDK).
  • Idealmente, un iPhone real emparejado con un Apple Watch real. Los simuladores ayudan, pero no cubren todos los tipos de transferencia.

Paso 1: Instala el plugin en el móvil

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

El plugin se activa WCSession en cuanto se carga, así que no hay llamada a init. Empieza leyendo el estado de la conexión:

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 referencia completa de API está en la docs del plugin y la página del plugin.

Paso 2: Añade un target de watchOS en Xcode

Abre el proyecto de iOS con bunx cap open ios. Los 8 proyectos de Capacitor creados con Swift Package Manager se abren App.xcodeproj; los proyectos antiguos de CocoaPods se abren App.xcworkspace. Cualquiera de los dos funciona.

  1. Elige Archivo > Nuevo > Target, selecciona la watchOS pestaña y luego app.
  2. Nómbralo, por ejemplo MyWatch, y usa SwiftUI como interfaz.
  3. En la hoja de opciones, asegúrate de que la app de watch esté incrustada en tu App target como su companion. Esa configuración es la que hace que Xcode incruste la app de watch en el build de iPhone y establezca el bundle identifier del companion.
  4. Deja las escenas de notificación y las complicaciones desactivadas a menos que las necesites ahora. Puedes añadir widgets más tarde.
  5. Acepta el prompt para activar el nuevo scheme.

La regla del bundle identifier

El bundle ID de la app del reloj debe empezar con el de la app del iPhone. Si tu app es com.example.shop, Xcode propone com.example.shop.watchkitapp. Mantén ese prefijo. Si más adelante cambias el bundle ID de iOS, cambia también el del reloj, o el build no pasará la validación y WatchConnectivity no emparejará las dos apps.

En Signing & Capabilities, selecciona el mismo equipo para ambos targets. Con la firma automática, Xcode registra el segundo App ID y el perfil de aprovisionamiento por ti.

Añade CapgoWatchSDK al target del reloj

  1. Archivo > Añadir dependencias de paquetes.
  2. Introduce https://github.com/Cap-go/capacitor-watch.git.
  3. Usa “Up to Next Major Version” de 8.0.0.
  4. Cuando Xcode pregunte qué productos añadir, selecciona solo CapgoWatchSDK and assign it to the watch target, no la app de iOS. El lado de iOS ya recibe el plugin mediante cap sync.

Paso 3: Escribe la app del reloj en SwiftUI

El SDK expone WatchConnector.shared, una ObservableObject con propiedades isReachable, isActivated, lastMessage, y applicationContext publicadas. Actívalo al iniciar la app:

import SwiftUI
import CapgoWatchSDK

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

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

Una pantalla pequeña que muestra datos enviados desde el teléfono y devuelve una acción:

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"
            }
        }
    }
}

El async variante de sendMessage envía con un manejador de respuesta, para que el teléfono lo reciba como un messageReceivedWithReply evento. La variante basada en callbacks sendMessage(_:replyHandler:errorHandler:) te permite omitir la respuesta.

Si prefieres callbacks de delegados en lugar de observar propiedades publicadas, implementa WatchConnectorDelegate y establece WatchConnector.shared.delegate. Tiene métodos para mensajes, mensajes con manejador de respuesta, contexto de la aplicación, información del usuario, conectividad y activación.

Paso 4: Habla con el reloj desde TypeScript

Registra los listeners temprano, por ejemplo en el bootstrap de tu app, para no perder eventos que lleguen al inicio.

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() },
  });
}

Responde siempre a messageReceivedWithReply. El reloj está esperando, y una solicitud sin respuesta termina en un error de timeout en el lado del reloj.

¿Qué método de transferencia deberías usar?

WatchConnectivity te ofrece tres canales, y el plugin mapea cada uno:

Método Entrega Requiere que el watch esté accesible Ideal para
sendMessage Inmediato Sí, de lo contrario se rechaza Toques en botones, live updates con ambas apps abiertas
updateApplicationContext Prevalece el último valor, se entrega cuando se puede No Estado actual: contadores, ajustes, usuario logueado
transferUserInfo En cola, se entregan en orden No Eventos que deben llegar sí o sí: entrenamientos registrados, registros creados

Un patrón que funciona bien: usa el contexto de la aplicación como fuente de verdad para lo que muestra el reloj, usa sendMessage solo para interacciones mientras la app del reloj está abierta, y usa transferUserInfo para cualquier cosa que no puedas permitirte perder.

Limitaciones a tener en cuenta

  • Tamaño del payload. WatchConnectivity rechaza los payloads demasiado grandes con payloadTooLarge, y Apple no publica un número fijo de bytes para cada canal. Mantén los payloads pequeños: IDs, contadores, strings cortos. En su lugar, obtén el contenido pesado desde tu API.
  • Tipos de payload. Los datos deben ser compatibles con property-list: strings, números, booleanos, fechas, data, arrays y diccionarios. null los valores de JavaScript no son válidos. Elimínalos antes de enviarlos.
  • El teléfono debe estar ejecutando tu app para manejar los eventos. Los listeners viven en el runtime de JavaScript de Capacitor. Si iOS ha terminado la app, los eventos no llegarán a tu TypeScript hasta que la app vuelva a ejecutarse. El contexto y la información del usuario quedan en cola por el sistema, pero diseña la interfaz del reloj para que funcione con el contexto en caché.
  • Emparejamiento. Un Apple Watch solo se empareja con un iPhone. Los usuarios de Android no pueden usarlo, y la implementación de Android del plugin apunta a Wear OS en su lugar, cubierto en nuestra guía de Wear OS.
  • Independencia. Una app complementaria construida sobre WatchConnectivity depende del teléfono. Si el reloj debe funcionar lejos del teléfono, añade una segunda fuente de datos, como tu propio API a través de la conexión de red del reloj.

Pruebas en simuladores y dispositivos

Puedes iterar sobre el diseño de SwiftUI en el simulador del reloj, pero la transferencia de datos requiere un emparejamiento configurado.

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

Ejecuta la app de iOS en el simulador del iPhone emparejado y luego ejecuta el scheme de watch en el simulador de watch. Ambas apps deben estar instaladas antes isWatchAppInstalled se vuelve true. Apple indica que el Simulator no admite transferUserInfo, así que confirma esa ruta en hardware real.

Para dispositivos, conecta el iPhone, asegúrate de que el reloj tenga el modo desarrollador activado y selecciona el reloj como destino de ejecución para el esquema del reloj. Usa getInfo() como tu smoke test:

getInfo() resultado Significado
isPaired: false No hay ningún reloj emparejado con este teléfono
isWatchAppInstalled: false Falta la app del reloj o el bundle ID/ajuste de companion está mal
isReachable: false, el resto es true App del reloj no está en primer plano o fuera de rango
activationState: 2, todo es true Listo para sendMessage

Solución de problemas

isWatchAppInstalled se mantiene en false. Comprueba el prefijo del bundle ID del reloj y que el target del reloj esté embebido en el App target (Build Phases > Embed Watch Content) y reinstala ambas apps.

Los mensajes del reloj nunca llegan a JavaScript. Registra los listeners antes de que el reloj envíe y confirma que el reloj llama a WatchConnector.shared.activate() al iniciar. En el teléfono, revisa la consola de Xcode para ver el log de activación del plugin.

sendMessage rechaza con “Watch is not reachable”. Esperado cuando la app del reloj está cerrada. Usa updateApplicationContext.

replyToMessage parece ignorado. Pass the exact callbackId exacto del evento y responde rápido. El handler de respuesta del reloj tiene un timeout.

El build falla con «Embedded binary’s bundle identifier is not prefixed». El bundle ID del watch debe comenzar con el bundle ID de iOS.

Subida de archivo rechazada por falta de iconos. Los targets de Watch necesitan su propio icono de app en Assets.xcassets.

Publicar la app del reloj

La app de Watch no es un producto separado. Está embebida en el archive de iOS, se sube junto con él y aparece en el mismo registro de App Store Connect, donde agregas capturas de Apple Watch bajo la pestaña de watch. Lo que genere tu archive, ya sea Xcode local, CI o Build de Capgo, necesita un provisioning profile para ambos bundle IDs y debe usar Xcode 26.

Ten en cuenta una cosa para las actualizaciones. actualizaciones Live Update de Capgo puede enviar cambios al web bundle del teléfono sin revisión de la tienda, lo cual es útil cuando el lado TypeScript de tu watch bridge tiene un bug. El Swift code en el watch, y cualquier cambio al plugin nativo, sigue pasando por la App Store. Versiona tu formato de mensajes, por ejemplo con un v campo en cada diccionario, para que un bundle de teléfono más nuevo siga funcionando con un build de watch más antiguo.

Resumen

La división es simple una vez que lo aceptas: SwiftUI para la muñeca, Capacitor para todo lo demás, y tres canales de WatchConnectivity en medio. Instala @capgo/capacitor-watch, añade el target de watch con CapgoWatchSDK, mantén los payloads pequeños y prueba en hardware emparejado antes de enviar.

Live updates para apps Capacitor

Cuando un bug de la capa web está en producción, publica el fix a través de Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el flujo de revisión habitual.

Lo último de nuestro blog

Capgo te da la información clave que necesitas para crear una app móvil realmente profesional.