El Apple Watch no es un iPhone pequeño. Esta es la primera lección, y la más dolorosa, que aprenden los desarrolladores novatos. Es un dispositivo íntimo, de “vistazos” (glances), diseñado para interacciones de segundos, no de minutos.
Con la llegada de watchOS 10, Apple redefinió por completo el paradigma de navegación y diseño. Las aplicaciones ahora son más coloridas, usan más la pantalla completa y dependen menos de jerarquías complejas. Si estabas esperando el momento perfecto para saltar al desarrollo de relojes, es ahora.
En este tutorial masivo, vamos a construir una aplicación desde cero, pero no una simple “Hola Mundo”. Vamos a arquitecturar una app de Entrenamiento y Salud, explorando HealthKit, la Corona Digital, las Complicaciones y la conectividad con el iPhone.
CAPÍTULO 1: Configuración y Filosofía del Entorno
1.1. App Independiente vs. App Compañera (Companion)
Hasta hace unos años, una app de Apple Watch era un “parásito” del iPhone. El código corría en el teléfono y la interfaz se proyectaba en el reloj. Eso murió.
Hoy tienes dos opciones al abrir Xcode:
- Watch App for iOS App: Crea un target de iOS y uno de watchOS. Ambos se instalan juntos. Ideal si tu app necesita configuración compleja en el teléfono (banco, redes sociales).
- Watch App: Una aplicación totalmente independiente. Se descarga desde la App Store del reloj. Puede funcionar sin que el usuario tenga un iPhone cerca (gracias a LTE/Wi-Fi).
Para este tutorial, elegiremos la opción 1, ya que la mayoría de las apps comerciales requieren una contraparte en iOS para visualizar datos históricos.
1.2. La Anatomía de un Proyecto watchOS
Al crear el proyecto, notarás una estructura distinta:
- Target iOS: Tu app de teléfono estándar.
- Target Watch App: Contiene el código de la vista y los Assets (imágenes, colores) que viven en el reloj.
En SwiftUI, el punto de entrada es idéntico al de iOS:
@main
struct WatchApp: App {
var body: some Scene {
WindowGroup {
HomeView()
}
}
}1.3. La Regla de los 10 Segundos
Antes de escribir una línea de código, grábate esto: Si el usuario tarda más de 10 segundos en obtener valor de tu app, has fallado.
- Mal diseño: Abrir app -> Menú -> Ajustes -> Seleccionar -> Ver dato.
- Buen diseño: Abrir app -> Ver dato.
CAPÍTULO 2: Diseño UI en la Era de watchOS 10
Apple rediseñó watchOS 10 basándose en dos pilares: Legibilidad y Uso de Esquinas.
2.1. El NavigationStack en la Muñeca
A diferencia de iOS, donde el espacio vertical es infinito, en el Watch queremos información paginada o listas muy concisas.
struct HomeView: View {
var body: some View {
NavigationStack {
List {
NavigationLink(value: "start") {
Label("Iniciar", systemImage: "play.fill")
.font(.title3)
.padding()
}
.listRowBackground(
RoundedRectangle(cornerRadius: 20)
.fill(Color.blue.gradient)
)
NavigationLink(value: "history") {
Label("Historial", systemImage: "list.bullet")
}
}
.navigationTitle("FitWatch")
.navigationDestination(for: String.self) { value in
if value == "start" {
WorkoutView()
} else {
HistoryView()
}
}
}
}
}2.2. Contenedores y Colores
En watchOS 10+, ya no usamos fondo negro puro para todo. Se anima a los desarrolladores a usar gradientes que llenen la pantalla para indicar estado (ej: Verde para activo, Rojo para detener).
El modificador .containerBackground es nuevo y esencial:
// Ejemplo de una vista de detalle colorida
struct WorkoutView: View {
var body: some View {
VStack {
Text("154 BPM")
.font(.system(size: 60, weight: .bold, design: .rounded))
Text("Cardio Intenso")
.font(.caption)
}
.containerBackground(Color.orange.gradient, for: .navigation)
}
}CAPÍTULO 3: Inputs Específicos del Reloj
El Watch no tiene teclado (bueno, el Series 7+ tiene uno pequeño, pero no confíes en él para entradas largas). Tus inputs principales son:
3.1. La Corona Digital (Digital Crown)
Es la herramienta más precisa del dispositivo. En SwiftUI, leemos su rotación con .digitalCrownRotation.
struct VolumeControlView: View {
@State private var volume: Double = 5.0
var body: some View {
VStack {
Text("Volumen")
Text("\(Int(volume))")
.font(.largeTitle)
.foregroundStyle(.green)
Gauge(value: volume, in: 0...10) {
EmptyView()
}
.gaugeStyle(.accessoryLinearCapacity)
}
// $volume: La variable a modificar
// from/through: Rango de valores
// by: Saltos por cada "click" háptico
// sensitivity: Qué tanto hay que girar
.focusable()
.digitalCrownRotation($volume, from: 0, through: 10, by: 1, sensitivity: .low)
}
}3.2. Selección Háptica
El Watch tiene el Taptic Engine. Debes usarlo para confirmar acciones, ya que el usuario a menudo no está mirando la pantalla cuando pulsa un botón mientras corre.
import WatchKit
func triggerHaptic() {
WKInterfaceDevice.current().play(.success)
}Úsalo con moderación: .success, .failure, .click. No abuses de .notification o el usuario sentirá que su muñeca vibra sin razón.
CAPÍTULO 4: El Corazón de la Bestia – HealthKit
Para una app de Watch, HealthKit no es opcional. Es el sistema que almacena y lee datos biométricos de forma segura.
4.1. Configuración de Privacidad
Primero, en tu archivo Info.plist (del Target Watch), debes agregar:
NSHealthShareUsageDescription: “Necesitamos leer tu ritmo cardíaco para mostrarlo en pantalla.”NSHealthUpdateUsageDescription: “Necesitamos guardar tus entrenamientos.”
4.2. El HealthManager
Crearemos una clase singleton para manejar la lógica sucia.
import HealthKit
class HealthManager: ObservableObject {
static let shared = HealthManager()
let healthStore = HKHealthStore()
@Published var heartRate: Double = 0
private init() {}
func requestAuthorization() {
// Qué queremos leer
let readTypes: Set = [
HKQuantityType.quantityType(forIdentifier: .heartRate)!,
HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!
]
// Qué queremos escribir
let shareTypes: Set = [
HKObjectType.workoutType()
]
healthStore.requestAuthorization(toShare: shareTypes, read: readTypes) { success, error in
if success {
print("Autorizado!")
}
}
}
}4.3. Iniciando una Sesión de Entrenamiento (HKWorkoutSession)
Aquí es donde la magia ocurre. Una sesión de entrenamiento le dice al Watch: “Mantén los sensores encendidos y la app en primer plano”.
// Dentro de HealthManager
var session: HKWorkoutSession?
var builder: HKLiveWorkoutBuilder?
func startWorkout() {
let configuration = HKWorkoutConfiguration()
configuration.activityType = .running
configuration.locationType = .outdoor
do {
session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
builder = session?.associatedWorkoutBuilder()
builder?.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration)
session?.startActivity(with: Date())
builder?.beginCollection(withStart: Date()) { (success, error) in
// La recolección de datos ha comenzado
}
} catch {
print("Error al iniciar workout: \(error)")
}
}CAPÍTULO 5: Ciclo de Vida y “Always On”
El ciclo de vida en watchOS es brutal. El sistema intenta suspender tu app tan pronto como el usuario baja la muñeca.
5.1. Manejando el Estado
Usamos scenePhase igual que en iOS, pero con implicaciones diferentes.
@Environment(\.scenePhase) var scenePhase
.onChange(of: scenePhase) { newPhase in
if newPhase == .inactive {
// El usuario bajó la muñeca.
// Si no estamos en un Workout activo, la app se congelará pronto.
}
}5.2. Always On Display (AOD)
Desde el Series 5, la pantalla puede quedarse encendida. Tu app necesita adaptarse a esto atenuando el contenido y ocultando datos sensibles.
SwiftUI maneja gran parte de esto automáticamente, pero puedes usar la variable de entorno .isLuminanceReduced.
@Environment(\.isLuminanceReduced) var isLuminanceReduced
var body: some View {
TimelineView(.periodic(from: .now, by: 1.0)) { context in
VStack {
if isLuminanceReduced {
// Versión simplificada para ahorrar batería y evitar quemado de pantalla
Text("🏃♂️ Corriendo")
Text("10:05") // Hora simple sin segundos
} else {
// UI Completa con animaciones
AnimatedHeartRateGraph()
Text("10:05:32")
}
}
}
}CAPÍTULO 6: Conectividad con iPhone (WatchConnectivity)
A veces necesitas enviar datos pesados al iPhone o recibir configuración. WCSession es tu puente.
6.1. Configuración del Delegado
Debes implementar WCSessionDelegate tanto en el iPhone como en el Watch.
import WatchConnectivity
class PhoneConnector: NSObject, WCSessionDelegate, ObservableObject {
var session: WCSession
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
self.session.activate()
}
func session(_ session: WCSession, activationDidCompleteWith state: WCSessionActivationState, error: Error?) {
// Sesión lista
}
// Método para recibir datos del iPhone
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
DispatchQueue.main.async {
if let name = message["userName"] as? String {
print("Usuario recibido: \(name)")
}
}
}
// Stubs requeridos por el protocolo (aunque no se usen en Watch)
func sessionDidBecomeInactive(_ session: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) {}
}6.2. Modos de Transferencia
- sendMessage: Inmediato. Úsalo para acciones en tiempo real (ej: pausar música). Falla si la contraparte no está alcanzable.
- transferUserInfo: Cola de fondo. Se envía cuando el sistema lo permite. Ideal para sincronizar datos que no urgen.
- updateApplicationContext: El “estado actual”. Reemplaza datos anteriores. Ideal para mantener sincronizados los ajustes (ej: Unidades métricas vs imperiales).
CAPÍTULO 7: Complicaciones – El Santo Grial del Engagement
Las Complicaciones son los pequeños widgets en la esfera del reloj. Son la razón principal por la que los usuarios aman el Apple Watch.
7.1. ClockKit vs. WidgetKit
Antiguamente usábamos ClockKit. Desde watchOS 9, usamos WidgetKit, el mismo framework que en iOS. Esto simplifica todo.
7.2. Creando un Widget para Watch
Agrega un nuevo Target a tu proyecto de tipo “Watch Complication Extension”.
struct FitWatchComplication: Widget {
let kind: String = "FitWatchComplication"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
ComplicationView(entry: entry)
}
.configurationDisplayName("Mis Pasos")
.description("Muestra tus pasos actuales.")
// Definimos qué familias soportamos (Esquinas, Circular, Rectangular)
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryCorner])
}
}7.3. Adaptando la Vista
Debes usar WidgetFamily para saber cómo dibujar.
struct ComplicationView: View {
@Environment(\.widgetFamily) var family
var entry: SimpleEntry
var body: some View {
switch family {
case .accessoryCircular:
ZStack {
Circle().stroke(lineWidth: 4)
Text("\(entry.steps)")
}
case .accessoryRectangular:
HStack {
Image(systemName: "figure.walk")
VStack(alignment: .leading) {
Text("Hoy")
.font(.caption2)
Text("\(entry.steps) pasos")
.font(.headline)
}
}
case .accessoryCorner:
Text("\(entry.steps)")
.widgetLabel {
ProgressView(value: 0.5)
}
default:
Text("Fit")
}
}
}CAPÍTULO 8: Despliegue y Optimización
8.1. Tamaño del Binario
El Apple Watch tiene almacenamiento limitado y conexiones lentas.
- Elimina Assets no utilizados.
- Comprime imágenes png/jpg o usa vectores (SVG/PDF).
- Evita frameworks de terceros pesados si puedes hacerlo nativo.
8.2. Screenshots para la App Store
Este es un punto de dolor común. Xcode incluye simuladores para 41mm y 45mm (Series 7/8/9) y 49mm (Ultra).
- Obligatorio: Debes subir capturas de pantalla específicas para el Watch Series (pantalla rectangular redondeada) y opcionalmente para el Ultra.
- Diseño: Asegúrate de que las capturas muestren el contenedor de color y la interfaz de alto contraste.
Conclusión y Siguientes Pasos
Has recorrido un largo camino. Hemos pasado de configurar un proyecto a leer el ritmo cardíaco y dibujar widgets en la esfera del reloj.
Desarrollar para watchOS es un ejercicio de minimalismo y eficiencia. Te obliga a destilar tu producto a su esencia más pura. No se trata de cuánto tiempo pasa el usuario en tu app, sino de cuánto valor le das en el menor tiempo posible.
Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram










