← Todos los artículos

Cinco plataformas de Apple, tres archivos compartidos: cómo Return lleva SwiftUI multiplataforma a producción

Return, mi temporizador de meditación, funciona en cinco plataformas de Apple: iPhone, iPad, Mac, Apple Watch y Apple TV.1 El código base tiene 40 archivos Swift (sin contar los tests). Tres de ellos se comparten entre las cinco plataformas. El resto se reparte en targets de Xcode separados que duplican conceptos como TimerManager, AudioManager y ContentView en lugar de compartirlos mediante compilación condicional con #if os(...).

La tasa de reutilización ronda el 7,5 %, y es intencional.

Este ensayo trata sobre cómo es realmente publicar una app SwiftUI multiplataforma en 2026, por qué compartir código de forma agresiva está sobrevalorado y qué tienen en común los tres archivos que se comparten.

Mosaico de la plataforma iOS 26 en Apple Developer Mosaico de la plataforma iPadOS 26 en Apple Developer Mosaico de la plataforma macOS 26 en Apple Developer Mosaico de la plataforma watchOS 26 en Apple Developer Mosaico de la plataforma tvOS 26 en Apple Developer

Las cinco plataformas a las que apunta Return, tal como las presenta Apple en developer.apple.com. Cada una es un target de plataforma distinto en Xcode, no una bifurcación en tiempo de ejecución.

En resumen

  • Return: 18 archivos Swift en el target principal (iOS + iPadOS + macOS), 10 en el target de tvOS, 7 en el de watchOS, 2 de widgets (Live Activities) y 3 verdaderamente multiplataforma en Return/Shared/. Total: 40.
  • Los tres archivos compartidos son los que rozan la persistencia: MeditationSession, SessionStore, SessionHistoryView. Estado que viaja por iCloud, no UI que se adapta a la plataforma.
  • tvOS y watchOS son targets de Xcode separados, no ramas #if os(tvOS) dentro del target principal. Los modelos de control son demasiado distintos para caber en un solo ContentView.
  • Incluso dentro del target principal de iOS/iPadOS/macOS, los bloques #if os se multiplican: 10 en ContentView.swift, 8 en LiveActivityManager.swift, 8 en VideoBackgroundView.swift, 6 en AudioManager.swift.
  • La lectura honesta: compartir de forma agresiva entre cinco plataformas de Apple es un lastre de mantenimiento. Un núcleo compartido pequeño (la capa de persistencia) más interfaces separadas por plataforma se publica más rápido y se rompe menos que un archivo gigante plagado de #if.

Para los complementos específicos de cada plataforma, lee la matriz de plataformas de Apple, el contrato del entorno de ejecución de watchOS y los patrones de Liquid Glass en SwiftUI.

Los números

La forma del código base, medida en archivos Swift, después de podar los tests y los tests de UI:

Return/                            18 files   (iPhone + iPad + Mac, single target)
├── Shared/                         3 files     cross-platform truth   ├── MeditationSession.swift   ├── SessionStore.swift   └── SessionHistoryView.swift
├── ContentView.swift              (10 #if os branches)
├── TimerManager.swift             (2 #if os branches)
├── AudioManager.swift             (6 #if os branches)
├── HealthKitManager.swift
├── LiveActivityManager.swift      (8 #if os branches, iOS-only)
├── ThemeManager.swift
├── VideoBackgroundView.swift      (8 #if os branches)
├── GlassTextShape.swift           (Liquid Glass, see prior post)
├── GlassTimerText.swift
└──  (settings, theme, audio assets, etc.)

ReturnTV/                          10 files   (tvOS, separate target)
├── TVContentView.swift
├── TVTimerManager.swift            duplicates main TimerManager
├── TVAudioManager.swift            duplicates main AudioManager
├── TVDurationPicker.swift
├── TVFocusModifier.swift           tvOS button styles for focus
├── TVSettingsView.swift
└── ReturnWatch Watch App/              7 files   (watchOS, separate target)
├── WatchContentView.swift
├── WatchTimerManager.swift         duplicates main TimerManager
├── WatchAudioManager.swift         duplicates main AudioManager
├── WatchHealthKitManager.swift     duplicates main HealthKitManager (mostly)
├── WatchSettingsView.swift
└── ReturnWidgets/                      2 files   (Live Activity + bundle)
├── ReturnLiveActivity.swift
└── ReturnWidgetsBundle.swift

Cinco plataformas, tres archivos compartidos, dos targets separados por plataforma más un target de widgets, además de bastante compilación condicional dentro del target principal. La proporción de reutilización ronda el 7,5 %. La mayoría de los tutoriales sobre “SwiftUI multiplataforma” sugieren lo contrario: escribe un solo ContentView que se adapte a cada plataforma con @Environment(\.horizontalSizeClass) y #if os(...).2 Eso funciona con dos plataformas (iPhone + iPad). Se rompe con cinco.

Qué tienen en común los tres archivos compartidos

Return/Shared/MeditationSession.swift define el tipo por valor cercano a SwiftData:3

struct MeditationSession: Codable, Identifiable, Equatable {
    let id: UUID
    let startDate: Date
    let endDate: Date
    let durationSeconds: Int
    let sourceDevice: DeviceType
    var syncedToHealthKit: Bool

    enum DeviceType: String, Codable, CaseIterable {
        case iPhone, iPad, mac, appleTV, appleWatch
    }
}

El comentario de cabecera del archivo no es decorativo: // Add this file to: Return, ReturnTV, ReturnWatch Watch App targets. Los tres targets de Xcode referencian el mismo archivo fuente, sin symlinks ni paquete Swift de por medio. El sistema de compilación de Apple compila sin problema un mismo archivo en tres binarios.

SessionStore.swift es la capa de persistencia: una envoltura fina sobre NSUbiquitousKeyValueStore (el almacén clave-valor de iCloud de Apple) que lee y escribe arreglos de MeditationSession. La elección importa: la sincronización por almacén clave-valor le da a Return un historial de sesiones entre dispositivos sin tener que aprovisionar un contenedor de CloudKit, a cambio de que el almacén completo esté limitado a 1 MB en total.12 Para una lista de sesiones de meditación de unos pocos cientos de bytes cada una, ese tope sobra. SessionHistoryView.swift es una lista de SwiftUI que dibuja las sesiones. Los targets de iPhone, iPad, Mac, Watch y TV los usan de forma idéntica.

Lo que tienen en común estos tres archivos: describen estado, no interacción. Una MeditationSession es el mismo concepto en todos los dispositivos. La lista de sesiones pasadas se lee igual en todos los dispositivos. Ninguno de los dos involucra una superficie de control, un gestor de ventanas, una decisión de enrutamiento de audio, un motor de foco ni una corona digital. En cuanto un archivo necesita saber en qué plataforma se está ejecutando, deja de ser compartible.

Por qué el resto no se compartió

Tomemos TimerManager. La versión de iOS/iPadOS/macOS usa Timer.publish(every: 1, ...) y encauza las notificaciones por UserNotifications. La versión de tvOS (TVTimerManager) contempla el caso en que el usuario pausa con el Siri Remote y entra el protector de pantalla. La de watchOS (WatchTimerManager) delega en una WKExtendedRuntimeSession (a través de WatchSessionManager) para que el sistema mantenga la app receptiva mientras la pantalla se atenúa, y encamina la entrada por la corona digital en vez del táctil. Tres plataformas, tres comportamientos de temporizador profundamente distintos.

Podrías unificarlos como class TimerManager { #if os(watchOS) ... #elif os(tvOS) ... }. El resultado sería una clase con tres modos, cada uno con cuarenta líneas condicionadas por #if, donde tocar la ruta de iOS pone en riesgo la de watchOS. Eso es un horror de mantenimiento.

Tres clases separadas con tres nombres de archivo son más código en el disco y menos código en la cabeza. La duplicación que puedes leer le gana a la abstracción que no puedes.

La misma lógica se aplica a:

  • ContentView frente a TVContentView y WatchContentView: los modelos de navegación son distintos (basado en apilamiento en el iPhone, en foco en el TV, en lista en el Watch).
  • AudioManager frente a TVAudioManager y WatchAudioManager: las categorías de sesión de audio difieren, watchOS tiene reglas más estrictas para el audio en segundo plano y tvOS enruta distinto hacia AirPlay.
  • VideoBackgroundView tiene 8 ramas #if os(iOS) en el target principal (con un #elseif os(macOS) que las acompaña), que cubren recursos de video diferentes (fire_phone.mp4 frente a fire_mac.mp4), tipos de capa distintos y relaciones de aspecto distintas.4

Conviene aclarar algo: el target principal Return/ agrupa iOS, iPadOS y macOS. Esas tres plataformas comparten más código del que no comparten. El NavigationStack de SwiftUI funciona en las tres. .glassEffect() funciona en las tres. Las diferencias de gestión de ventanas son reales pero manejables dentro de un solo target. Fue en tvOS y watchOS donde tracé la línea del target separado.

El caso de tvOS: por qué el motor de foco obligó a un target aparte

La navegación en Apple TV se construye alrededor del motor de foco.5 Cada elemento de interfaz con el que el usuario puede interactuar se declara enfocable; las flechas del sistema en el Siri Remote mueven el foco entre elementos; al pulsar seleccionar se activa el elemento enfocado. SwiftUI en tvOS expone esto mediante .focusable(), .focusEffect y tipos ButtonStyle personalizados que responden a @Environment(\.isFocused) para lograr el efecto de inclinación en paralaje que usan las apps propias de Apple. Código real de producción en TVFocusModifier.swift:6

struct TVCapsuleButtonStyle: ButtonStyle {
    var accentColor: Color = .white
    @Environment(\.isFocused) private var isFocused

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .colorMultiply(isFocused ? focusedTextColor : accentColor)
            .background(
                Capsule().fill(isFocused
                    ? AnyShapeStyle(accentColor)
                    : AnyShapeStyle(.ultraThinMaterial))
            )
            .clipShape(Capsule())
            .scaleEffect(isFocused ? 1.1 : 1.0)
            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
            .shadow(color: .black.opacity(isFocused ? 0.3 : 0.1),
                    radius: isFocused ? 20 : 5, y: isFocused ? 10 : 2)
            .animation(.easeInOut(duration: 0.2), value: isFocused)
    }
}

El mismo archivo define además TVCircleButtonStyle para controles cuadrados o circulares. Ambos estilos invierten color y translucidez al recibir el foco: los botones sin foco se apoyan en .ultraThinMaterial, los enfocados se rellenan con el color de acento y suben escala y sombra. Para esta app, el patrón es estructuralmente propio de tvOS. @Environment(\.isFocused) está disponible en iOS, iPadOS, macOS, watchOS y tvOS,13 pero la navegación guiada por foco es el modelo de interacción principal solo en tvOS, donde el Siri Remote no produce ningún evento de puntero ni táctil. En el iPhone o el iPad, el control equivalente se resuelve por toque; en el Mac, al pasar el cursor o al hacer clic. Los estilos de botón de TVFocusModifier.swift asumen que el foco es la principal vía de interacción del usuario y diseñan toda la respuesta visual a su alrededor. No hay forma decente de escribir un ContentView que resuelva en un mismo lugar el táctil en iOS, el cursor en el Mac y la navegación por foco en tvOS. La estructura de la vista es genuinamente distinta: un ContentView de tvOS es un grafo de filas enfocables; uno de iOS, una pila de tocar y actuar.

Lo mismo pasa con el selector de duración. En el iPhone se desliza desde abajo y acepta toques. En el Apple TV es una fila horizontal de celdas enfocables que el usuario recorre con el control. TVDurationPicker.swift es un archivo propio porque el diseño de foco basado en celdas no tiene análogo en el iPhone. Meterlos a la fuerza en un solo archivo significaría dos interfaces sin relación pegadas con #if os(tvOS).

El caso de watchOS: sesiones de ejecución extendida, HealthKit y una superficie más pequeña

watchOS suma dos restricciones estructurales que las demás plataformas no tienen:

  1. WKExtendedRuntimeSession para mantener la app receptiva mientras la pantalla del reloj está atenuada.8 Sin ella, watchOS suspende la app con agresividad entre cada tic de segundo y el temporizador se desfasa. Return declara WKBackgroundModes: mindfulness en el Info.plist del target de watchOS para que el sistema reconozca el caso de uso y le conceda el presupuesto de ejecución; la sesión en sí se crea con el inicializador por defecto WKExtendedRuntimeSession().
  2. Sincronización por iCloud con NSUbiquitousKeyValueStore, no con WatchConnectivity.7 La sincronización del historial de sesiones de Return viaja sobre el mismo almacén clave-valor que usan los targets de iPhone, iPad y Mac, así que una meditación registrada en el reloj aparece en la vista de historial del iPhone sin ninguna mensajería directa reloj-teléfono. WatchConnectivity podría ser una opción futura para sincronizar estado en vivo, pero Return eligió el modelo más simple: cada dispositivo escribe en el mismo almacén clave-valor de iCloud y la siguiente lectura desde cualquier dispositivo ve la unión.

WatchTimerManager.swift es el temporizador del lado del reloj; delega el trabajo de ejecución extendida en WatchSessionManager, definido en ReturnWatchApp.swift como final class WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate. El TimerManager de iOS no tiene análogo porque las apps de iOS se mantienen receptivas en primer plano sin una sesión de ejecución explícita. Meter la lógica del reloj dentro del TimerManager de iOS con #if os(watchOS) implicaría que la ruta de iOS importe símbolos de WatchKit que nunca usa, además de que la ruta de watchOS necesita caminos de inicialización que la de iOS no tiene.

WatchHealthKitManager.swift es una variante reducida del HealthKitManager principal. Registra los minutos de atención plena de la misma manera, pero la experiencia del aviso de autorización es distinta (el reloj no puede mostrar una HealthKitPermissionSheet). La clase del Watch mide más o menos la mitad que la principal.

Qué pasa dentro del target principal de iOS/iPadOS/macOS

Ni siquiera dentro del target principal la reutilización es automática. ContentView.swift tiene diez bloques #if os(macOS) o #if !os(macOS); LiveActivityManager.swift, ocho; VideoBackgroundView.swift, ocho; AudioManager.swift, seis. Las Live Activities son una función exclusiva del iPhone, así que todo LiveActivityManager va envuelto en #if os(iOS). El selector de duración del iPhone usa una disposición distinta a la del iPad y el Mac, de modo que ContentView tiene ramas de disposición paralelas.

El patrón que ha funcionado: #if os(...) para deltas pequeños de plataforma (comportamiento de teclado distinto, espaciado distinto, API ausente) y target separado para deltas estructurales grandes (foco frente a táctil, sesión de entrenamiento frente a temporizador). El umbral que terminé usando es “más de unas 10 líneas de bifurcación”. Por debajo de eso, la compilación condicional está bien. Por encima, el archivo está haciendo dos trabajos a la vez y el segundo pertenece a otro target.

Cuándo no publicar en las cinco plataformas

La evaluación honesta.

Sáltate el Apple Watch si tu app es densa en información. La pantalla de 46 mm no tiene espacio para una lista de 30 elementos, un selector de duración y una página de configuración. Return sobrevive en watchOS porque la interacción central es un botón (iniciar/detener un temporizador). Una app de productividad, una de finanzas o una cargada de contenido multimedia no lo lograrán.

Sáltate el Apple TV si tu app es interactiva. El TV es para experiencias ambientales (un temporizador corriendo en una pantalla al otro lado de la habitación, reproducción de música). Cualquier cosa que exija entrada frecuente del usuario está peleando contra la plataforma. Return está en tvOS porque “pon un temporizador de 20 minutos y mira el fuego en la pantalla” es exactamente el caso ambiental correcto. Una app de notas sería un suplicio.

Sáltate el Mac si tu app es una interfaz pensada para el teléfono. SwiftUI funciona en Mac, pero el modelo de apilamiento de NavigationStack parece de juguete al lado de una barra lateral de Mac de verdad. Si la app se sentiría a medio construir en Mac, publica con Catalyst (que convierte la app de iPad) o sáltate el Mac por completo hasta que puedas construir una interfaz nativa.

Sáltate el iPad si no has hecho adaptación por clases de tamaño. Una app de iPhone estirada para llenar un iPad se ve barata. El iPad necesita como mínimo un NavigationSplitView con barra lateral; idealmente, un diseño real de dos paneles. Return usa vistas divididas en iPad y pilas en iPhone. El código está en el mismo target, pero la interfaz es genuinamente distinta.

La regla que tracé: publica en una plataforma cuando la interacción central de la app encaje con el modelo de entrada de esa plataforma. Publica un temporizador de meditación en el Apple Watch (un toque para empezar). Publica un temporizador de meditación en el Apple TV (ponlo y olvídate). No publiques un tablero kanban en ninguno de los dos.

Qué viaja sin esfuerzo

Las tres cosas que sí se compartieron entre las cinco plataformas en Return:

  1. El modelo de datos (MeditationSession). El struct es idéntico en todas las plataformas, se sincroniza vía NSUbiquitousKeyValueStore y cualquier plataforma puede leer lo que escribió cualquier otra.
  2. La vista de historial de sesiones (SessionHistoryView). Una List de sesiones pasadas se dibuja igual en iPhone, iPad, Mac, Apple Watch y Apple TV. La List de SwiftUI es una de las pocas primitivas que se adapta con limpieza a los cinco factores de forma.
  3. La envoltura de persistencia (SessionStore). Las lecturas y escrituras son agnósticas de la plataforma; el almacenamiento subyacente (NSUbiquitousKeyValueStore) es la misma API en todas partes.

Tres conceptos. Estado, renderizado de listas y persistencia. Todo lo que tenga estado y sea presentacional, y no dependa de un modelo de entrada específico del hardware, es compartible. Todo lo que toque entrada, foco, enrutamiento de audio, tamaño de pantalla o ejecución en segundo plano, no lo es.

Este patrón aparece en la guía de desarrollo iOS con agentes, donde defendí lo mismo con otras palabras: las partes de una app de iOS que un agente puede escribir comparten la mayor parte de su código con las que escribe una persona; las partes que requieren criterio humano (firma, pulido visual, rendimiento) son justamente las que tampoco se comparten bien entre plataformas.9 Los dos límites coinciden. Ambos hablan de dónde empieza a importar el conocimiento del dominio.

Cuánto cuesta ser multiplataforma

El ROI es asimétrico. Añadir iPad a una app de iPhone cuesta quizá un 20 % más de código (ramas por clase de tamaño, vista dividida en algunos lugares). Añadir Mac a ese mismo target suma otro 15-20 % (ramas #if os(macOS), barra de menús, gestión de ventanas). Cada target importante añade unos 10 archivos en una app pequeña.

El Apple Watch y el Apple TV son los caros. Añadir watchOS a Return exigió 11 archivos nuevos en un target separado, incluidos gestores dedicados de audio, temporizador y HealthKit. Añadir tvOS exigió 10 archivos nuevos en otro target separado, incluidas la gestión de foco y un selector de duración a medida. Entre los dos casi duplicaron la superficie de Swift para lo que, a nivel de funcionalidad para el usuario, es la misma app.

La decisión de publicar en las cinco no fue “queremos ser multiplataforma porque sí”. Fue una serie de decisiones independientes: Apple Watch porque los temporizadores de meditación pertenecen genuinamente a la muñeca, Apple TV porque el formato de pantalla ambiental le sienta bien a las sesiones largas en una habitación, Mac porque algunos usuarios meditan en su escritorio entre reuniones. Cada plataforma se ganó su target teniendo un caso de uso real.

Si una función no se gana su target, la jugada más barata es saltarse la plataforma y redoblar la apuesta en aquellas donde la app es excelente.

Qué significa esto para tu app

Tres conclusiones.

  1. Por defecto, un target por grupo principal de plataformas. iOS + iPadOS + macOS en un target funciona porque la interacción central (táctil + cursor) es parecida. tvOS en un target aparte. watchOS en un target aparte. Cada target separado cuesta unos 10 archivos, pero te salva de una clase-dios con ramas #if que crecen sin límite.
  2. Comparte estado con ganas, interacción no. Los structs de modelo Codable, las envolturas de persistencia y los renderizados con List viajan casi gratis. Los gestores de temporizador, los de audio y las vistas de contenido, no.
  3. Que cada plataforma se la gane. No publiques en watchOS porque puedes. Publica cuando la interacción central de tu app encaje con el modelo de entrada de la plataforma. Sáltate el resto.

Este patrón convive con las otras tres superficies sobre las que he escrito para la misma familia de apps: App Intents tipados para Apple Intelligence, servidores MCP para agentes de distintos LLM, y Liquid Glass para la persona frente al dispositivo. La capa más externa de esa misma pila es la plataforma: en qué pantallas se ejecuta siquiera la app. Elígela con la misma deliberación con que eliges la superficie de IA.

Preguntas frecuentes

¿Por qué no usar un paquete Swift para el código compartido?

Lo consideré. Para tres archivos, un paquete Swift añade más ceremonia de la que ahorra. El sistema de compilación de Xcode 26 compila sin problema un mismo archivo fuente en varios targets cuando marcas las casillas de Target Membership. Un paquete añade un Package.swift aparte, un target de tests aparte y un paso de indirección que hay que sortear en cada refactorización. Para un núcleo compartido pequeño, gana la respuesta simple.10

¿SwiftData funciona en watchOS y tvOS?

SwiftData está disponible en iOS 17+, macOS 14+, watchOS 10+ y tvOS 17+, es decir, todas las plataformas a las que apunta Return.11 El struct MeditationSession es Codable a secas, no un @Model, porque Return usa NSUbiquitousKeyValueStore para sincronizar el historial de sesiones en vez de un contenedor de SwiftData. El patrón funciona igual con tipos @Model: el archivo del modelo se comparte y el contenedor de persistencia cambia por plataforma si hace falta.

¿Conviene usar Mac Catalyst o un target nativo de Mac?

Catalyst es la herramienta correcta cuando la app de iPad es lo bastante buena como para que una versión de Mac reconstruida con Catalyst pase por nativa. El target principal de Return es un verdadero target multiplataforma (no Catalyst), hecho con SwiftUI para iOS, iPadOS y macOS en un solo binario. La interfaz de Mac usa #if os(macOS) para dibujarse distinto que en el iPad: barra lateral en vez de hoja, equivalentes de teclado en los botones, etc. Catalyst habría sido más simple, pero la interfaz de Mac habría parecido una app de iPad en un Mac, que es el modo de fallo por el que Catalyst es más conocido.

¿Vale la pena publicar en Apple TV para una app pequeña?

Probablemente no. Las apps de Apple TV tienen casos de uso muy específicos (ambiental, multimedia, juego casual). Si tu app no encaja en alguno de esos, la audiencia de la plataforma es demasiado pequeña para justificar los 10 archivos Swift por app. Return apunta a tvOS precisamente porque las sesiones largas de meditación en una pantalla al otro lado de la habitación son uno de los pocos casos cercanos a la productividad que encajan en la plataforma.

¿Cuánto se tarda en publicar en las cinco plataformas?

Es difícil dar una cifra precisa; depende de la app. Return salió multiplataforma desde el primer día en vez de ir sumando plataformas de a poco, lo cual es más rápido que adaptar después. Como regla gruesa: un MVP solo para iPhone más soporte de iPad más soporte de Mac es aproximadamente 1,5 veces el tiempo de solo iPhone. Añadir Apple Watch suma otro 0,5. Añadir Apple TV, otro 0,5. Así que un primer lanzamiento en cinco plataformas es más o menos 2,5 veces el esfuerzo de solo iPhone, con la salvedad de que fue una construcción asistida por agentes en la que la mayor parte del código duplicado la editó en bloque Claude Code y no se tecleó a mano.

Referencias


  1. Return del autor, una app de temporizador de meditación publicada en la App Store el 21 de abril de 2026. Targets nativos: iOS 26+, iPadOS 26+, macOS 26+, watchOS 26+, tvOS 26+. SwiftUI de punta a punta. NSUbiquitousKeyValueStore para el historial de sesiones entre dispositivos. 

  2. Apple Developer, “Configuring a Multi-Platform App” y la sesión “SwiftUI essentials” de la WWDC 2024. La guía por defecto de Apple se inclina por un solo target con adaptación dirigida por el entorno; la ruta de múltiples targets que toma este artículo es una desviación deliberada. 

  3. Código de producción en Return/Return/Shared/MeditationSession.swift, SessionStore.swift, SessionHistoryView.swift. El comentario de cabecera en MeditationSession.swift dice: “Add this file to: Return, ReturnTV, ReturnWatch Watch App targets.” 

  4. Código de producción en Return/Return/VideoBackgroundView.swift (8 ramas #if os(iOS) más una rama #elseif os(macOS)), Return/Return/ContentView.swift (10 ramas #if os), Return/Return/AudioManager.swift (6 ramas #if os), Return/Return/LiveActivityManager.swift (8 ramas #if os, archivo exclusivo de iOS). Los conteos de ramas provienen de ejecutar grep -Ec '^\s*#if os\\(' <file>

  5. Apple Developer, Human Interface Guidelines, “Focus interactions”. El motor de foco de tvOS es un modelo de navegación fundamentalmente distinto del táctil en iOS o del puntero en Mac. 

  6. Código de producción en Return/ReturnTV/TVFocusModifier.swift. Define dos tipos ButtonStyle (TVCapsuleButtonStyle y TVCircleButtonStyle) que envuelven @Environment(\.isFocused) para invertir color y translucidez al recibir el foco y aplicar escala y sombra. 

  7. Apple Developer, “WatchConnectivity”. El framework para la comunicación entre un iPhone y un Watch emparejados; Return no lo usa para sincronizar sesiones, y se apoya en el almacén clave-valor de iCloud. 

  8. Apple Developer, “WKExtendedRuntimeSession” y la clave de Info.plist “WKBackgroundModes”. El valor mindfulness está documentado así: “Enables extended runtime sessions for silent meditation”, justo lo que necesita un temporizador de meditación. Return crea una WKExtendedRuntimeSession() por defecto y declara WKBackgroundModes: mindfulness en el Info.plist del target de watchOS. Código de producción: Return/ReturnWatch Watch App/ReturnWatchApp.swift define WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate; WatchTimerManager.swift le delega el trabajo de ejecución extendida. 

  9. Análisis del autor en Cómo construir apps de iOS con agentes de IA, la guía práctica sobre desarrollo iOS asistido por agentes a lo largo de 8 apps en producción. 

  10. Apple Developer, “Configuring a Multi-Platform App”. La pertenencia a targets permite compilar un mismo archivo fuente en varios targets sin un paquete Swift. La herramienta adecuada para núcleos compartidos pequeños. 

  11. Apple Developer, “SwiftData” — disponibilidad por plataforma. Disponible en iOS 17+, iPadOS 17+, macOS 14+, watchOS 10+, tvOS 17+ y visionOS 1+, es decir, las cinco familias de plataformas de Apple. 

  12. Apple Developer, “NSUbiquitousKeyValueStore”. El almacén clave-valor de iCloud de Apple para sincronizar pequeñas cantidades de estado entre los dispositivos de una persona. El tamaño total del almacén está limitado a 1 MB sumando todas las claves, según los límites publicados por Apple. Código de producción: Return/Return/Shared/SessionStore.swift

  13. Apple Developer, EnvironmentValues.isFocused. Disponible en iOS 14+, iPadOS 14+, macOS 11+, tvOS 14+ y watchOS 7+. La API es multiplataforma; lo que cambia es si el foco es la señal de navegación principal para el usuario. 

Artículos relacionados

La matriz de plataformas de Apple: qué objetivos merecen qué app

iOS, iPad, Mac, Watch, Vision, TV. Seis plataformas, seis obligaciones. Elegir los objetivos de Apple es una decisión de…

22 min de lectura

HealthKit + SwiftUI en iOS 26: autorización, tipos de muestra y patrones multiplataforma de dos apps en producción

Patrones de producción de Water (hidratación, HKQuantitySample) y Return (mindfulness, HKCategorySample): UX de permisos…

19 min de lectura

Instalar y actualizar Codex CLI: Mac, Linux, Windows

Todas las formas de instalar, actualizar, fijar versión y desinstalar la CLI de OpenAI Codex -- script de instalación, n…

20 min de lectura