← Todos los articulos

Las Live Activities son una máquina de estados, no una insignia

La Live Activity en Return parece un número de cuenta regresiva en la pantalla de bloqueo y en la Dynamic Island.12 No es un número. Es una máquina con cinco estados de ciclo de vida, tres rutas externas de descarte y una ruta de inicio reentrante que tiene que defenderse de sí misma. Los patrones que aparecen abajo son los que sobrevivieron a producción. La nota de honestidad brutal al final dice lo que aún no sé.

Lancé una v1 que trataba la Live Activity como una insignia. El “tiempo restante actual” era datos; el resto era decoración. Esa versión tenía tres bugs que detecté en TestFlight y uno que detecté en producción:

  1. Tocar iniciar mientras el inicio ya estaba en curso creaba una segunda actividad que dejaba huérfana a la primera.
  2. La cuenta regresiva se renderizaba correctamente en la Dynamic Island, pero la vista de la pantalla de bloqueo evaluaba endTime <= Date() para los temporizadores en pausa y mostraba 0:00 hasta que el usuario reanudaba.
  3. La Live Activity permanecía visible mucho después de que el usuario reiniciara el temporizador porque la política de descarte era .default, que Apple mantiene visible durante un tiempo de hasta cuatro horas.
  4. (Producción.) En configuraciones regionales con idiomas de derecha a izquierda (árabe, hebreo), los dígitos se renderizaban al revés en la región compact-trailing de la Dynamic Island. Dígitos latinos, diseño RTL. La corrección fue una sola línea.

Cada uno de esos era un bug de máquina de estados. El número de la cuenta regresiva estaba bien. El número no es el producto. El producto es el estado.

La máquina de estados que aparece abajo es lo que sobrevivió a esos bugs.

TL;DR

  • El LiveActivityManager que está en producción expone 5 métodos de transición (startActivity, updateActivity, showCycleComplete, showFinalCompletion, endActivity) más 1 lectura (hasActiveActivity). Las 224 líneas de producción protegen contra un peligro específico dentro de startActivity: llamadas concurrentes de inicio más verificaciones de cancelación en cada límite de await dentro de ese método.3
  • El ContentState lleva 6 campos: endTime, currentCycle, totalCycles, isPaused, isCompleted, remainingSeconds. Los primeros cinco son las etiquetas de la máquina de estados. El sexto (remainingSeconds) es un respaldo de visualización estática que el timerInterval en vivo de ActivityKit no puede atender.
  • La decisión de la política de descarte es la verdadera decisión del producto. .immediate para reinicio del usuario, .after(Date().addingTimeInterval(3)) para finalización, nunca el predeterminado del sistema.
  • La región compact-trailing de la Dynamic Island necesita .environment(\.layoutDirection, .leftToRight) en el texto del temporizador para mantener los dígitos latinos en LTR bajo configuraciones regionales del sistema en RTL.

La máquina de estados

La Live Activity en producción tiene un estado inactivo, tres estados activos que el usuario puede observar, un estado terminal y una compuerta reentrante que el desarrollador debe observar:

┌──────────────────────────────────────────────────────────────────┐
│                  Lifecycle states                                 │
├──────────────────────────────────────────────────────────────────┤
│  IDLE          currentActivity == nil; no Live Activity present   │
│  RUNNING       isPaused=false, endTime > Date()                   │
│  PAUSED        isPaused=true, remainingSeconds=N                  │
│  CYCLE_END     isPaused=false, endTime <= Date(), isCompleted=false│
│  COMPLETE      isCompleted=true (terminal; transitions to IDLE)   │
└──────────────────────────────────────────────────────────────────┘
              │
              ↓
┌──────────────────────────────────────────────────────────────────┐
│             Dismissal policies (Apple)                            │
├──────────────────────────────────────────────────────────────────┤
│  .immediate            user reset                                  │
│  .after(now + 3s)      completion display window                   │
│  .default              system decides; can stay up to 4 hours      │
└──────────────────────────────────────────────────────────────────┘

Reentrancy gate inside startActivity():
  isStartingActivity flag + cancellable startActivityTask
  prevents two concurrent startActivity() calls from creating
  two Live Activities for one timer. Cancellation checks across
  each await keep the in-flight task safe to abort.

La ruta de renderizado verifica isPaused primero; ese orden es lo que evita que un temporizador en pausa se renderice como CYCLE_END cuando el tiempo del reloj ha cruzado endTime.7

Los nombres de los estados no son etiquetas sobre el número. Los nombres de los estados son el contrato entre LiveActivityManager (el lado de la app, donde viven mis vistas SwiftUI) y ReturnLiveActivity (la extensión del widget, donde el proceso de Apple renderiza la superficie).

El contrato es TimerActivityAttributes.ContentState, los 6 campos:3

public struct ContentState: Codable, Hashable {
    var endTime: Date
    var currentCycle: Int
    var totalCycles: Int?
    var isPaused: Bool
    var isCompleted: Bool = false
    var remainingSeconds: Int = 0
}

Cada transición de estado muta esta estructura y le pide a ActivityKit que la entregue a través de los límites del proceso a la extensión del widget. El widget entonces vuelve a renderizar. No hay memoria compartida. No hay callback. Hay una estructura Codable que cruza un límite de proceso en cada transición.

Ese hecho descarta cualquier cosa que yo pudiera querer hacer con clausuras, view models, observable objects o propiedades calculadas. El estado tiene que ser expresable como datos serializables. Si no se puede codificar, no puede transicionar.

El inicio reentrante

Las Live Activities tienen un límite duro en actividades concurrentes y un límite blando en lo que pasa si llamas a Activity.request dos veces en curso. El límite duro está bien documentado.4 El límite blando es “la segunda llamada puede tener éxito y crear una huérfana”. La huérfana es la Live Activity que ya no está asociada con currentActivity en tu manager. Sobrevive. No tiene una ruta de regreso a tu código. Eventualmente se descarta por su propio temporizador de obsolescencia. El usuario ve un temporizador duplicado hasta entonces.

La huérfana fue el bug de la v1 con el que Return salió a producción. La corrección es la compuerta reentrante más una Task cancelable en LiveActivityManager.swift:3

private var isStartingActivity = false
private var startActivityTask: Task<Void, Never>?

func startActivity(...) {
    #if os(iOS)
    guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
    guard !isStartingActivity else { return }
    isStartingActivity = true

    startActivityTask?.cancel()

    startActivityTask = Task {
        defer {
            isStartingActivity = false
            startActivityTask = nil
        }
        guard !Task.isCancelled else { return }

        await endActivity()  // explicit cleanup of any prior state

        guard !Task.isCancelled else { return }

        // ... build attributes + contentState ...

        do {
            let activity = try Activity.request(...)
            guard !Task.isCancelled else { return }
            currentActivity = activity
        } catch {
            // log; flag clears via defer
        }
    }
    #endif
}

Tres cosas sobre ese patrón que la documentación no menciona:

La bandera isStartingActivity es la protección activa; startActivityTask?.cancel() es limpieza defensiva. La bandera corta cualquier segunda llamada a startActivity mientras la primera está en curso, así que en realidad no se compite por la ruta pública. El baile de cancelar-y-reemplazar sigue importando porque la Task en curso es asíncrona y puede sobrevivir a un llamador de vida corta; la cancelación previene que una Task obsoleta continúe después de que el llamador haya seguido adelante.

Las verificaciones guard !Task.isCancelled en cada límite de await. La cancelación es cooperativa en Swift. Aun cuando se llame a cancel, la Task sigue ejecutándose hasta que verifica explícitamente. Cada await es una oportunidad para verificar. Sin las verificaciones posteriores al await, una Task cancelada sigue construyendo el estado de la actividad, llama a Activity.request y crea silenciosamente una huérfana al tener éxito.

El defer limpia la bandera antes de que el cuerpo de la Task se complete. Sin defer, un return temprano (de la verificación de cancelación) deja isStartingActivity = true permanentemente y la actividad nunca vuelve a iniciarse hasta el relanzamiento de la app. La bandera es un candado; el candado tiene que liberarse en cada ruta de salida.

El argumento pushType: nil. Return no usa actualizaciones de Live Activity por APNs. La app actualiza la actividad localmente vía activity.update. Si necesitas actualizaciones impulsadas por push (seguimiento de entregas, marcadores deportivos, datos en tiempo real), el tipo es pushType: .token y el contrato es dramáticamente más complejo.5 Las actualizaciones locales son más simples y cubren cualquier flujo de trabajo de temporizador / contador / app única.

El problema de la pausa

ActivityKit incluye una hermosa vista Text(timerInterval: Date()...endTime, countsDown: true) que renderiza una cuenta regresiva en vivo sin ninguna actualización de la app.6 Estableces el tiempo de finalización, el sistema renderiza un temporizador en vivo. Sin Timer.publish, sin actualización del widget, sin consumo de batería.

Eso es fantástico cuando el temporizador está corriendo. Está mal cuando el temporizador está en pausa.

El texto timerInterval cuenta hacia endTime independientemente de cualquier señal de “pausa” en el estado. No hay un modo “congelado a las 10:23” en la API de Apple. Si pasas endTime = Date().addingTimeInterval(623) y el usuario pausa en la marca de 10:23, el texto del temporizador sigue contando hacia cero en el widget. El campo de estado dice pausado. El widget renderiza corriendo.

La corrección es renderizar dos vistas diferentes desde el mismo estado:7

if context.state.isPaused {
    // static text
    Text(formatTime(context.state.remainingSeconds))
        .monospacedDigit()
} else if context.state.endTime > Date() {
    // live countdown
    Text(timerInterval: Date()...context.state.endTime, countsDown: true)
        .monospacedDigit()
} else {
    // post-end static
    Text("0:00")
        .monospacedDigit()
}

El renderizado de doble vía es la razón por la que el ContentState lleva remainingSeconds como un campo separado. Es redundante cuando el temporizador está corriendo (el sistema lo calcula a partir de endTime). Es la única fuente de verdad cuando el temporizador está en pausa. Las dos mitades de la estructura sirven a dos modos de renderizado diferentes; el booleano isPaused selecciona entre ellos.

Las políticas de descarte

activity.end(_:dismissalPolicy:) toma uno de tres valores ActivityUIDismissalPolicy, y elegir mal es lo que hizo que mi v1 se quedara en la pantalla de bloqueo del usuario por lo que se sintió como una eternidad después de un reinicio:13

Política Cuándo usarla Lo que obtienes
.immediate Reinicio del usuario, error, app en segundo plano sin actividad que rastrear La actividad desaparece ahora. Sin ventana de gracia
.after(date) Visualización de finalización: “tu meditación está completa” necesita ser legible por un momento. La fecha debe estar dentro de la ventana de cuatro horas que Apple permite La actividad muestra el estado final, luego se descarta en date
.default Cuando genuinamente quieres que las heurísticas de Apple decidan El sistema la mantiene visible “por algún tiempo” (palabras de Apple), hasta cuatro horas después de que se llame a end

Return usa .after(Date().addingTimeInterval(3)) para la ruta de finalización natural:3

await activity.end(
    .init(state: contentState, staleDate: nil),
    dismissalPolicy: .after(Date().addingTimeInterval(3))
)

Tres segundos es el tiempo que un usuario necesita para mirar la pantalla de bloqueo, registrar que el temporizador terminó y sentir la satisfacción de la marca de verificación. Menos de tres es brusco. Más de tres se siente como si la actividad no supiera que terminó.

Para un reinicio activado por el usuario, la llamada es dismissalPolicy: .immediate. Sin ventana. El usuario ya lo sabe.

La elección equivocada en la v1 fue .default. Para un temporizador de meditación completado, el sistema mantenía la actividad visible lo suficiente como para que los usuarios pensaran que la app no había registrado la finalización en absoluto. La documentación de Apple dice que .default mantiene la actividad finalizada “visible por algún tiempo” hasta cuatro horas;13 la postura correcta para un temporizador es hacer que el descarte sea explícito.

La región compacta de la Dynamic Island

La Dynamic Island tiene tres modos de renderizado y necesitas los tres incluso para un temporizador simple:2

  • Compact (forma predeterminada de la Dynamic Island): icono al inicio + temporizador al final
  • Minimal (cuando otra Live Activity compite por la misma Dynamic Island): solo icono al inicio
  • Expanded (presión larga): cuatro regiones nombradas (leading, trailing, center, bottom)

El patrón que se ganó su lugar en Return es hacer que la vista expandida sea casi idéntica a la compacta:8

DynamicIsland {
    DynamicIslandExpandedRegion(.leading) {
        Image("AppIconSmall")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 16, height: 16)
            .clipShape(RoundedRectangle(cornerRadius: 4))
    }
    DynamicIslandExpandedRegion(.trailing) {
        TimerText(...)
    }
    DynamicIslandExpandedRegion(.center) { EmptyView() }
    DynamicIslandExpandedRegion(.bottom) { EmptyView() }
} compactLeading: {
    Image("AppIconSmall")...
} compactTrailing: {
    TimerText(...)
} minimal: {
    Image("AppIconSmall")...
}

La mayoría de los tutoriales sobre Live Activity se inclinan hacia la vista expandida como el diseño “real”, con contenido rico en la región bottom. Para un temporizador de meditación, la expansión es peso muerto. El usuario abre la vista expandida con una presión larga, y la presión larga ya les da la retroalimentación háptica de que algo pasó. Agregar contenido hace que la expansión diga algo que el usuario no pidió. Las regiones vacías en el modo expandido no son una falla del diseño; son el diseño.

El bug de RTL

El bug de producción. Usuarios de árabe y hebreo en iOS reportaron que el temporizador compact-trailing de la Dynamic Island renderizaba los dígitos al revés. La cadena numérica latina 5:23 se renderizaba como 32:5 porque el diseño compact-trailing heredaba la configuración RTL del sistema.

SwiftUI hereda la dirección de diseño del sistema dentro del proceso del widget, así que el texto del temporizador de la Dynamic Island tomaba RTL cuando el teléfono del usuario estaba configurado en árabe o hebreo. Los numerales latinos deberían renderizarse de izquierda a derecha incluso dentro de una UI que de otra forma sería RTL. La corrección es fijar la dirección de diseño en las vistas de texto numérico:7

.environment(\.layoutDirection, .leftToRight)

La anulación va en las vistas Text numéricas dentro de TimerText (Dynamic Island compact / expanded) y dentro de la vista de la pantalla de bloqueo, no en toda la vista. Los dígitos latinos se leen de izquierda a derecha independientemente de la configuración regional del usuario; las etiquetas de ciclo como “Cycle 2 of 3” permanecen localizadas para que sigan la dirección de diseño del sistema.

El bug no aparece en TestFlight con configuraciones regionales nacionales. Aparece en el momento en que un usuario real de RTL abre el temporizador. La lección: incluye la anulación de entorno fijada en LTR en cada vista de texto con dígitos latinos en cualquier Live Activity que pueda ejecutarse en configuraciones regionales RTL.

La historia de la localización

TimerActivityAttributes lleva un campo languageCode: String que la app establece en la creación de la actividad:9

let attributes = TimerActivityAttributes(
    timerDuration: duration,
    languageCode: settings.appLanguage  // app's selected language, not system's
)

La extensión del widget lee esto para renderizar cadenas localizadas:

private var locale: Locale {
    let code = context.attributes.languageCode
    return code.isEmpty ? .current : Locale(identifier: code)
}

private func localized(_ key: String.LocalizationValue) -> String {
    String(localized: key, locale: locale)
}

Por qué la app pasa su propio código de idioma en lugar de dejar que el widget lea Locale.current: la extensión del widget se ejecuta en su propio proceso. Su Locale.current es la configuración regional del sistema, no la configuración regional seleccionada por la app. Si un usuario ha configurado Return en coreano mientras su iPhone está en inglés, el widget hablaría inglés sin esta anulación. La preferencia de idioma de la app viaja en los atributos de la actividad; el widget la respeta.

Localizable.xcstrings vive en el target del widget junto al de la app, pero son archivos separados. Las cadenas usadas en el widget tienen que existir en ReturnWidgets/Localizable.xcstrings aun cuando la misma cadena exista en Return/Localizable.xcstrings. Olvidar esto significa que el widget recurre al idioma de desarrollo mientras la app habla coreano.

Lo que construiría diferente

Hacer ContentState más pequeño. Seis campos son demasiados. La redundancia entre endTime y remainingSeconds es el precio de sortear el modo sin pausa en timerInterval. Si empezara de nuevo, llevaría un único enum displayMode (running, paused(remainingSeconds: Int), cycleEnd, complete) y dejaría que el código de renderizado despache según el caso. Seis campos son más difíciles de mantener correctamente mutados a través de cinco métodos de transición que cuatro casos.

Agregar botones interactivos de Live Activity (iOS 17+). Return actualmente no expone controles de pausa/reanudar en la Dynamic Island. El usuario tiene que abrir la app para pausar. iOS 17 agregó Button(intent:) para App Intents dentro de Live Activities.10 Un control de pausa interactivo es la extensión obvia y lo siguiente que lanzaré para Return.

Live Activities con actualizaciones por push para sincronización de temporizadores entre dispositivos. Return sincroniza sesiones a través de iPhone, iPad, Watch y Apple TV vía NSUbiquitousKeyValueStore (cubierto en Cinco plataformas Apple, tres archivos compartidos). Hoy la actividad se inicia localmente desde la app de iPhone o iPad y se actualiza localmente. Un usuario que inicie un temporizador en Apple Watch idealmente podría ver la Live Activity reflejarlo en el iPhone en tiempo real. Push de APNs a la Live Activity es la ruta.5 No lo he construido.

Cuándo no usar Live Activities

Estado transitorio de una sola vez. Un toast de “¡guardado!” no merece una Live Activity. El sistema tiene un banner. Úsalo.

Datos que cambian frecuentemente sin una dimensión temporal. Las Live Activities funcionan mejor para cosas con un ancla temporal clara (un temporizador, una ETA de entrega, un reloj de juego, una duración de llamada telefónica). Los tickers de acciones y los marcadores deportivos funcionan porque tienen una ventana de sesión. Un dashboard de propósito general no.

Apps sin un caso de uso de pantalla de bloqueo / standby. Las Live Activities requieren una inversión de ingeniería real (configuración del target, diseño de ContentState, decisiones de política de descarte, manejo de RTL, plomería de localización). Las apps que el usuario abre directamente sin nunca consultar la pantalla de bloqueo durante el uso no son la forma correcta. Un editor de fotos no la necesita. Un rastreador de entrenamientos sí.

En superficies que no son iOS, con advertencias. El LiveActivityManager de Return entrega su implementación detrás de #if os(iOS) porque el temporizador se inicia desde la app de iPhone o iPad. ActivityKit en sí mismo describe el banner de la pantalla de bloqueo, la Dynamic Island, el Smart Stack de Apple Watch, Mac y CarPlay como superficies de presentación; iOS 26 expandió varias de esas.4 watchOS aún tiene sus propias complications API para el renderizado en pantalla completa. macOS tiene apps de barra de menú. iPadOS soporta Live Activities desde iPadOS 17 sin región de Dynamic Island. El manager de Return tiene 8 guardas #if os(iOS) a lo largo de un archivo de 224 líneas.

Lo que el patrón significa para apps que se lanzan en iOS 26+

Dos conclusiones.

  1. Trata la Live Activity como una máquina de estados, no un número. La máquina de estados tiene estados claros, transiciones claras y reglas de descarte claras. El número en la pantalla es un renderizado de un estado. Acierta primero los estados.

  2. La protección de reentrada es el bug que aún no has tenido. Cada manager de Live Activity que he visto en el mundo real que no implementa isStartingActivity + Task cancelable ha lanzado al menos un bug de actividad huérfana. La protección son 6 líneas. Escríbelas una vez.

Empareja este post con mis escritos anteriores para la misma familia de apps: App Intents tipados para Apple Intelligence; servidores MCP para agentes cross-LLM; patrones de Liquid Glass para la capa visual; lanzamiento multiplataforma para alcance entre dispositivos. Las Live Activities son la capa de pantalla de bloqueo de iOS y Dynamic Island del mismo stack. El conjunto completo vive en el hub de la Serie de Ecosistema Apple. Para el contexto más amplio de iOS con agentes de AI, consulta la guía de Desarrollo de Agentes para iOS.

FAQ

¿Cuál es la diferencia entre Live Activities y los widgets de WidgetKit?

Los widgets de WidgetKit se renderizan en intervalos definidos por TimelineProvider; el sistema decide cuándo refrescar y el widget se vuelve a renderizar a partir de una línea de tiempo estática.11 Las Live Activities se renderizan en respuesta a llamadas específicas activity.update(...) impulsadas por la app y viven durante la duración de la actividad subyacente (un temporizador, una entrega, un entrenamiento). Ambos se entregan en el target de extensión del widget; la diferencia es el modelo de activación.

¿Las Live Activities funcionan en iPad?

Sí, en iPadOS 17+. El banner de la pantalla de bloqueo es la superficie principal de renderizado; el iPad no tiene Dynamic Island. El mismo código de ActivityConfiguration funciona; solo espera que las regiones de la Dynamic Island nunca se rendericen en el iPad.

¿Una Live Activity puede sobrevivir al proceso de mi app?

Sí. Una vez que Activity.request tiene éxito, ActivityKit es dueño de la actividad. El sistema puede terminar el proceso de la app; la actividad continúa renderizándose en la pantalla de bloqueo y la Dynamic Island hasta que la termines explícitamente (o hasta que las reglas de obsolescencia del sistema la descarten). Las llamadas explícitas a endActivity() importan por esa razón; sin un fin explícito al reiniciar la app, la actividad sobrevive al temporizador.

¿Por qué el post no cubre Live Activities con actualizaciones por push?

No he lanzado Live Activities con actualizaciones por push en Return. Según la regla de género para este cluster: los posts de código en producción solo documentan lo que hace el código en producción. Las actualizaciones por push están listadas en “Lo que construiría diferente”; un post futuro las cubrirá después de que las lance.

¿Cuál es el diseño de archivos real para Live Activities en una app SwiftUI?

Tres piezas:3712

  • En el target principal de la app: LiveActivityManager.swift (gestiona el ciclo de vida de la actividad), TimerActivityAttributes.swift (la estructura ActivityAttributes compartida con el widget; ambos targets compilan este archivo).
  • En un target de extensión de widget: ReturnLiveActivity.swift (la conformidad con Widget con cuerpo ActivityConfiguration), ReturnWidgetsBundle.swift (el @main WidgetBundle).
  • Configuración: Info.plist con NSSupportsLiveActivities = YES en el target de la app.

El target de extensión del widget necesita imports de ActivityKit y WidgetKit. TimerActivityAttributes es el único archivo compartido a través de ambos targets; todo lo demás está aislado por target.


La Live Activity no es un número en la pantalla de bloqueo. Es una máquina de estados que cruza un límite de proceso en cada transición. Acierta los estados, protege la reentrada, elige la política de descarte a propósito y fija la dirección de diseño. El número se cuida solo.

Referencias


  1. Return del autor, un temporizador de meditación SwiftUI publicado en la App Store el 21 de abril de 2026, disponible para iPhone, iPad, Mac, Apple Watch y Apple TV. Las Live Activities se entregan solo en el target de iOS. 

  2. Apple Developer, “ActivityKit framework”. Banner de la pantalla de bloqueo, modos compact / minimal / expanded de la Dynamic Island, ciclo de vida de la actividad. Disponible en iOS 16.1+; Dynamic Island disponible en iPhone 14 Pro y posteriores. 

  3. Código en producción en Return/Return/LiveActivityManager.swift (224 líneas, 8 bloques #if os(iOS)) y Return/Return/TimerActivityAttributes.swift (43 líneas). Compartido entre el target de la app y el target de extensión del widget vía membresía de target. 

  4. Apple Developer, “Displaying live data with Live Activities”. Límites de concurrencia, plataformas soportadas (iOS 16.1+, iPadOS 17+), llave NSSupportsLiveActivities de Info.plist. 

  5. Apple Developer, “Updating and ending your Live Activity with ActivityKit push notifications”. La ruta pushType: .token requiere una llave de autenticación APNs separada, registro de token de push del lado del servidor y un protocolo de actualización diferente de las llamadas locales activity.update(...)

  6. Apple Developer, “Text(timerInterval:pauseTime:countsDown:showsHours:)”. Temporizador de cuenta regresiva renderizado en vivo por el sistema; se renderiza sin actualizaciones de la app mientras la actividad está corriendo. 

  7. Código en producción en Return/ReturnWidgets/ReturnLiveActivity.swift (232 líneas). La conformidad con Widget de la extensión del widget con cuerpo ActivityConfiguration<TimerActivityAttributes>. La vista TimerText en las líneas 61-102 maneja el renderizado de tres estados: pausado / corriendo / post-fin. 

  8. Apple Developer, “DynamicIsland”. Las cuatro regiones expandidas nombradas (leading, trailing, center, bottom) más tres vistas de modo compacto (compactLeading, compactTrailing, minimal). 

  9. La extensión del widget se ejecuta en su propio proceso y hereda la configuración regional del sistema, no la configuración regional seleccionada por la app. Las apps que soportan cambio de idioma dentro de la app (Return soporta 27 idiomas) deben pasar el código de idioma a través de ActivityAttributes para que el widget pueda renderizar en el idioma elegido por el usuario. Patrón: Locale(identifier: context.attributes.languageCode) en lugar de Locale.current

  10. Apple Developer, “Button(intent:)”. Disponible en vistas de widget y Live Activity desde iOS 17+. Conecta App Intents con controles de la pantalla de bloqueo / Dynamic Island sin requerir que la app esté en primer plano. 

  11. Apple Developer, “TimelineProvider”. El modelo de actualización de widgets que precede a las Live Activities; entradas precalculadas con ventanas de recarga gestionadas por el sistema. 

  12. Código en producción en Return/ReturnWidgets/ReturnWidgetsBundle.swift (16 líneas). El @main WidgetBundle que registra ReturnLiveActivity como el único widget de la extensión del widget. Patrón requerido para extensiones de widget; el bundle es lo que el sistema carga. 

  13. Apple Developer, “ActivityUIDismissalPolicy”. Tres casos: .default, .immediate, .after(_:). Apple establece que .default mantiene una Live Activity finalizada visible “por algún tiempo” hasta cuatro horas, y .after(_:) acepta una fecha dentro de la misma ventana de cuatro horas. 

Artículos relacionados

La superficie de widgets de iOS 26: un App Intent, muchos lugares

Los widgets de iOS 26, los controles del Centro de Control y las Live Activities son todos superficies de App Intents. U…

10 min de lectura

Liquid Glass en SwiftUI: tres patrones de lanzar Return en iOS 26

El Liquid Glass de Apple es una API de SwiftUI de una sola línea. Tres patrones de Return van más allá de .glassEffect()…

20 min de lectura

Ingeniería de bucles: los bucles ganan donde verificar es barato

La ingeniería de bucles, contrastada con las transcripciones completas de Boris Cherny: cada bucle que menciona tiene un…

18 min de lectura