Return...

Un temporizador zen de meditación y concentración en cinco pantallas: iPhone, iPad, Apple Watch, Apple TV y Mac.

Publicado el 21 de abril de 2026. Una sola base de código. Veintisiete idiomas, incluyendo árabe y hebreo. Cuatro temas, tres campanas, cero analítica. Lo que sigue es cómo se construyó: las decisiones técnicas, las concesiones de diseño y el largo y silencioso proceso de depurar cientos de gotas de agua generadas por IA hasta quedarnos con una.

Universal

Una base de código, cinco pantallas.

Return es la primera app que he publicado capaz de funcionar en todas las clases de pantalla de Apple desde un único proyecto de Xcode: iPhone, iPad, Apple Watch, Apple TV y Mac. Cincuenta y siete archivos Swift, unas 12 700 líneas de código y cero dependencias externas. SwiftUI puro, AVFoundation, HealthKit, ActivityKit y WidgetKit.

La forma ingenua de hacerlo es tener un único TimerManager universal con ramas #if para cada diferencia de plataforma. No lo hice así. Return incluye tres clases de temporizador (TimerManager en iOS y macOS, TVTimerManager en tvOS, WatchTimerManager en watchOS) que comparten la semántica del estado pero respetan aquello en lo que cada plataforma es realmente buena. Live Activities solo en iOS. HealthKit solo donde existe la API. Sesiones de ejecución extendida solo en el Watch. Cada manager es más corto y más honesto de lo que sería una única clase polimórfica.

Return ejecutándose en iPhone 17 Pro Max con el tema Fuego
iPhone
Return ejecutándose en macOS con el tema Fuego
Mac
Return ejecutándose en Apple Watch Series 11 con el tema Fuego
Watch
Return ejecutándose en Apple TV con el tema Fuego
Apple TV
Return ejecutándose en iPad Pro de 13 pulgadas con el tema Fuego
iPad

Compartido donde importa.

Una sola carpeta Shared/ contiene las piezas en las que todos los destinos deben coincidir: el modelo de datos MeditationSession, el envoltorio de iCloud SessionStore y SessionHistoryView. La configuración se sincroniza entre el Watch y el teléfono mediante un App Group (group.com.941apps.Return). El resto es específico de cada plataforma de forma deliberada.

El ejemplo más claro es la línea que decide si una sesión ya se ha registrado en HealthKit. El iPhone escribe directamente, así que «sincronizado» es verdadero en el momento en que termina la sesión. El Mac y el TV no pueden escribir en HealthKit en absoluto, así que «sincronizado» es falso hasta que el iPhone recoge la sesión pendiente más adelante. La misma intención, el booleano contrario, una sola #if:

Swift · TimerManager.swift:120-138
/// Save session to SessionStore for cross-device sync and HealthKit syncing
private func saveSessionToStore(startTime: Date, endTime: Date) {
    // On iOS: if healthKitEnabled, we save directly to HealthKit, so mark as synced
    // On Mac: if healthKitEnabled, we want to sync to iPhone, so mark as NOT synced
    #if os(iOS)
    let alreadySynced = settings.healthKitEnabled
    #else
    let alreadySynced = !settings.healthKitEnabled
    #endif

    let session = MeditationSession(
        startDate: startTime,
        endDate: endTime,
        sourceDevice: .current,
        syncedToHealthKit: alreadySynced
    )

    SessionStore.shared.addSession(session)
}

Vuelvo a este patrón constantemente: el menor número de líneas que todavía deje legible la intención. Cuando el mismo booleano significa cosas distintas en plataformas distintas, escríbelo como booleanos distintos. La #if pasa a formar parte de la documentación.

Localizada

Veintisiete idiomas, con soporte de derecha a izquierda.

Return es la primera app de Apple que he publicado en todos los idiomas que me importaban. Veintisiete idiomas pasaron por una revisión completa, incluyendo árabe y hebreo. Todo vive en un único archivo Localizable.xcstrings, lo cual es menos heroico de lo que parece. Xcode hace casi todo el trabajo si aceptas dejar de escribir cadenas a mano.

Pantalla de inicio de Return, tema Agua, inglés
EnglishInicio · Agua
Pantalla de inicio de Return, tema Fuego, japonés
日本語Inicio · Fuego
Pantalla de inicio de Return, tema Bosque, chino simplificado
简体中文Inicio · Bosque
Pantalla de ajustes de Return, alemán
DeutschAjustes
Pantalla de permisos de HealthKit de Return, coreano
한국어HealthKit

El soporte RTL es gratis si dejas de pelearte con él.

SwiftUI trata .leading y .trailing como direcciones semánticas, en lugar de .left y .right como posiciones fijas. Diseña una pantalla con direcciones semánticas una sola vez y la misma pantalla se reflejará automáticamente en árabe, hebreo, persa o urdu sin una ruta de código dedicada. Las etiquetas de ajustes se invierten, el chevron de retroceso se invierte, las posiciones de los interruptores se invierten. Los iconos de los temas (gota, llama, hoja) se quedan donde están. No escribí ni una línea de código RTL para lograr este comportamiento.

Pantalla de inicio de Return, tema Bosque, inglés, disposición de izquierda a derecha
Inglés · LTR
Pantalla de inicio de Return, tema Bosque, árabe, disposición de derecha a izquierda
Árabe · RTL
Pantalla de inicio de Return, tema Bosque, hebreo, disposición de derecha a izquierda
Hebreo · RTL

Una excepción que detecté justo antes de publicar: SwiftUI también aplica la dirección de disposición a las vistas Text, lo que hizo que la primera versión de las capturas en árabe y hebreo mostrara el temporizador como «00:02» en lugar de «20:00» — dígitos latinos dispuestos de derecha a izquierda. Un único modificador .environment(\.layoutDirection, .leftToRight) en cada vista Text que contenga tiempo o contenido numérico lo soluciona. Las capturas de arriba son de la versión que incluye ese modificador.

El conjunto de capturas lo generó fastlane ejecutando las mismas pruebas de interfaz con distintos argumentos -AppleLanguages. El patrón effectiveLocale de la propia app lee la bandera, reconstruye la jerarquía de vistas y captura el resultado. Un solo helper, veintisiete idiomas, cuatro clases de dispositivo, todo en una única ejecución nocturna.

Swift · ReturnWatchApp.swift:92-111
/// The locale to use for the app - either user-selected or system default
/// In snapshot mode, always use system language (set by -AppleLanguages)
/// to allow screenshot generation for different locales
private var effectiveLocale: Locale {
    if isSnapshotMode || appLanguage.isEmpty {
        if let preferredLanguage = Locale.preferredLanguages.first {
            return Locale(identifier: preferredLanguage)
        }
        return .current
    }
    return Locale(identifier: appLanguage)
}

var body: some Scene {
    WindowGroup {
        WatchContentView()
            .preferredColorScheme(.dark)
            .environment(\.locale, effectiveLocale)
            .id(appLanguage) // Force rebuild when locale changes
    }
}

El .id(appLanguage) es el detalle que se gana su sitio. Sin él, SwiftUI guarda en caché la jerarquía de vistas anterior y las cadenas no se actualizan al cambiar de idioma en tiempo de ejecución. Con él, todo el árbol se descarta y se reconstruye, y todo vuelve a leer sus cadenas localizadas automáticamente. Una línea, una categoría de errores eliminada.

HealthKit

Minutos de atención plena, por fin.

La app nativa de Mindfulness del Watch de Apple limita las sesiones integradas de Reflexionar y Respirar a cinco minutos. La propia API de HealthKit no tiene ese límite. Aceptará sin problema cualquier HKCategorySample en el que la fecha de finalización sea posterior a la de inicio. El límite vive en la interfaz, no en el sistema. Return pone un selector de 5 a 60 minutos en todos los dispositivos y registra lo que realmente has estado sentado.

Swift · HealthKitManager.swift:92-103
/// Save a mindful session with the given start and end time
func saveMindfulSession(start: Date, end: Date) async -> Bool {
    guard isAvailable else { return false }

    // Don't save if end is before or equal to start
    guard end > start else { return false }

    let sample = HKCategorySample(
        type: mindfulType,
        value: HKCategoryValue.notApplicable.rawValue,
        start: start,
        end: end
    )
    ...
}

La única validación es end > start. Eso es todo lo que el propio HealthKit valida. La API de Apple siempre ha estado dispuesta a registrar una meditación de cuarenta y cinco minutos. Simplemente faltaba el botón para pedirla.

Multidispositivo sin HealthKit en tres de ellos.

El Mac y el Apple TV no tienen HealthKit en absoluto. La respuesta obvia es «pues no te molestes en registrar las sesiones ahí». La respuesta menos obvia y correcta es registrarlas de todos modos, en el almacén clave-valor de iCloud, y dejar que el teléfono las recoja la próxima vez que se active. El SessionStore de Return es ese almacén compartido, MeditationSession.syncedToHealthKit es la bandera pendiente y HealthKitManager.syncPendingSessions() se ejecuta cada vez que la app de iOS vuelve a primer plano.

SessionStore
Almacén clave-valor de iCloud
Sesiones pendientes
El iPhone escribe en HealthKit ♥
Gráfico de barras de Minutos de atención plena de Apple Health que muestra un promedio de 20 minutos durante un mes
Minutos de atención plena de Apple Health, vista de barras. La propia app de Mindfulness de Apple tiene como tope una sesión de Reflexionar de cinco minutos. Al almacén subyacente no le importa lo que escribas en él.
Calendario de Minutos de atención plena de Apple Health que muestra 18 días de práctica en las últimas 4 semanas
Los mismos datos, vista de calendario: 18 días en las últimas 4 semanas, cada sesión registrada desde Return.
Pantalla de historial de sesiones de Return mostrando una lista de meditaciones de 20 minutos
El propio historial de sesiones de Return. Cada dispositivo contribuye, y cada sesión lleva un marcador de origen.

Esta es la pieza que creo que Apple debería publicar: un escritor multiplataforma decente de Minutos de atención plena que no requiera que el teléfono esté activo cuando quieres meditar en un Mac. Hasta que lo hagan, Return lo hace.

Generativo

De dónde salió el agua.

Cuatro temas. Cuatro bucles ambientales. Tres campanas. Todo generado, casi todo descartado. Los vídeos son de Midjourney, el audio es de ElevenLabs, y el trabajo que importaba no eran los prompts. Era la edición. Mirar una rejilla de doscientas gotas de agua y elegir la única que hace bucle limpio sin una costura visible. Escuchar cuarenta variaciones de una campana de templo hasta que una tiene el ataque correcto y el decaimiento correcto y no suena como la notificación de un teléfono.

Hoja de contactos de Midjourney: cientos de variaciones de gotas de agua, algunas marcadas con corazones y triángulos de reproducción
Agua · 128 mostradas
Hoja de contactos de Midjourney: decenas de variaciones de fuego
Fuego · 96 mostradas
Hoja de contactos de Midjourney: variaciones de dosel arbóreo y hojas
Bosque · 60 mostradas
Hoja de contactos de Midjourney: exploración de nubes y cielo que no llegó a publicarse
Exploración inédita · 128 mostradas

Cada mosaico es una generación. Los corazones son los que sobrevivieron a una primera pasada. Los triángulos de reproducción son los que llevé a vídeo. Se publicaron cuatro temas. Todo lo demás se quedó en la rejilla, y ese es el objetivo del proceso: la proporción importa.

Las campanas siguieron el mismo arco en audio. Prompt, escuchar, refinar, volver a hacer prompt. Me quedé con tres: Singing Bowl, Temple Bell, Soft Chime. Cada una iterada hasta que dejó de sonar sintética.

No voy a fingir que cuente el total de generaciones. Cientos por tema es honesto. La disciplina no está en los prompts. Está en descartar todo lo que es meramente bueno y quedarse solo con lo que puede estar detrás de un temporizador durante veinte minutos silenciosos sin llegar nunca a convertirse en eso que notas.

Práctica

Por qué un temporizador y no un maestro.

Esta parte es personal. Construí Return porque ya tengo una práctica de meditación y no conseguía encontrar un temporizador que se quitara de en medio. Lo que yo practico es el Zen japonés en su corriente marcial: Takuan, Yagyu, Musashi, Dogen, Hakuin. No la atención plena terapéutica que publican las grandes apps. Intención distinta, textura distinta.

Lo que rota a lo largo de una semana cualquiera:

  • Susokukan (conteo de la respiración). Contar del uno al diez en la respiración, volver al uno siempre que se pierda la cuenta. Fundamento. Concentración, joriki, primero.
  • Shikantaza (solo sentarse). Sin objeto. Sin conteo, sin pregunta, sin visualización. La mente que no se fija. La forma central de zazen de Dogen y la aproximación formal más cercana al estado que realmente quiero.
  • Koan. Principalmente el Mu de Joshu. Una pregunta que no se puede resolver pensando, sostenida hasta que el pensar se rinde.
  • Maranasati (contemplación de la muerte). En el marco del Hagakure. Usada con moderación. La supervivencia tensa la mente; esto corta a través de ella.
  • Isshin (una sola mente). Territorio de Takuan y Yagyu: relajada pero comprometida, asentada pero móvil. El puente entre el cojín y lo que venga después.
  • Días de integración. Gratitud, compasión, linaje. Jihi. Katsujinken: la espada que da vida, no la espada que mata. Los sábados, normalmente.
  • Sakki (conciencia de la intención hostil). Cinco minutos de escucha a campo abierto al final de cada sesión. Saca el shikantaza del cojín y lo pone a prueba en entornos ordinarios.

La rotación no es rígida. Conteo de la respiración cuando necesito estabilizar. Koan cuando necesito atravesar algo. Shikantaza cuando necesito descansar en la apertura. Contemplación de la muerte cuando las apuestas necesitan aclararse. La variedad pertenece al entrenamiento.

Return es un temporizador porque no necesito un maestro en mi teléfono. Necesito algo que sostenga el reloj para que yo no tenga que hacerlo, que marque el principio y el final con una campana que respeto, y que se quite de en medio entre ambos. Si ya tienes una práctica, probablemente eso sea también lo que quieres. Si eres totalmente nuevo, busca un maestro en una sala. Luego vuelve.

Contención

Lo que no está en Return.

Return no es Calm. No es Headspace. No hay un narrador británico que te guíe suavemente por un escaneo corporal. No hay un avatar de dibujos celebrando tu racha. No hay una suscripción que desbloquee nuevos programas guiados. Return es un temporizador. La idea es que si ya tienes una práctica, no necesitas un maestro en la app. Necesitas una herramienta que sostenga el tiempo por ti y se quite de en medio.

  • Sin voz guiada ni narración
  • Sin rachas, puntuaciones ni gamificación
  • Sin suscripción ni compras dentro de la app
  • Sin anuncios, jamás
  • Sin analítica; la app no rastrea nada
  • Sin inicio de sesión social ni compartir
  • Sin pantallas insistentes ni modales de arranque
  • Sin patrones oscuros en el flujo de compra, porque no hay flujo de compra

Lo que hay en Return, mantenido deliberadamente pequeño: cuatro modos de repetición (Una vez, Hasta detener, Hasta la hora, Repetir N veces), una pausa de respiración de dos segundos entre ciclos, de una a tres campanadas en cada transición, elección entre tres campanas, cuatro temas, activación opcional de HealthKit y un selector de idioma. Ese es el producto entero.

El coste de ser así de estricto se ve en el modelo de ajustes. Cada preferencia de cara al usuario se limita a un rango válido mediante la propia propiedad, no mediante validación en la interfaz. La validación en la interfaz es otro patrón oscuro si no tienes cuidado. El getter de bellRepeatCount no puede devolver nada que no sea 1, 2 o 3. Escribir 0 o 47 en el @AppStorage subyacente lo recorta silenciosamente al rango permitido.

Swift · Settings.swift:74-81
@ObservationIgnored
@AppStorage("bellRepeatCount") private var _bellRepeatCount = 1

/// Validated bell repeat count (1-3)
var bellRepeatCount: Int {
    get { max(1, min(3, _bellRepeatCount)) }
    set { _bellRepeatCount = max(1, min(3, newValue)) }
}

Return cuesta 2,99 $. Lo pagas una vez y es tuyo. Sin costes de servidor que mantener, sin suscripción que renovar, sin canalización de analítica mirando lo que haces. El producto es el producto. Si quieres la versión larga de por qué sigo construyendo apps así, lee Minimum Worthy Product y The Steve Test. La versión corta vive en esta sección.

Return.

Disponible ya en la App Store para iPhone, iPad, Apple Watch, Apple TV y Mac.