Piensa en la última vez que pediste comida a domicilio. Seguramente miraste el móvil diez veces para ver por dónde iba el repartidor. Las Live Activities existen justo para eso: que la información importante esté en la pantalla de bloqueo o en la Dynamic Island sin tener que abrir la app.
Para el usuario es comodísimo y para ti, como desarrollador, es una forma muy directa de que tu app esté presente cuando importa. En esta guía vemos cómo funcionan, cuándo tienen sentido y cómo implementarlas paso a paso con ActivityKit y SwiftUI, incluidas las actualizaciones desde tu servidor.
¿Qué son exactamente las Live Activities y la Dynamic Island?
Las Live Activities son widgets interactivos y persistentes que se ubican en la pantalla de bloqueo (Lock Screen) y en el modo En Reposo (StandBy). A diferencia de los widgets tradicionales, que actualizan sus datos a través de una línea de tiempo predefinida (TimelineProvider), las Live Activities están diseñadas para eventos que tienen un inicio, un desarrollo continuo y un final definido, con actualizaciones en segundos.
Por su parte, la Dynamic Island es la evolución de hardware y software introducida a partir del iPhone 14 Pro. Funciona como una extensión directa de las Live Activities: cuando el usuario sale de la aplicación mientras una actividad está activa, la interfaz se traslada elegantemente a la parte superior de la pantalla, adaptando su forma según el contexto.
Ambas características comparten el mismo núcleo de datos, lo que significa que diseñar para una Live Activity implica diseñar automáticamente para la Dynamic Island.
El motor técnico: ActivityKit y SwiftUI
Para dar vida a estas experiencias, Apple divide las responsabilidades en dos herramientas esenciales:
- ActivityKit: Gestiona el ciclo de vida de la actividad (iniciar, actualizar y finalizar) y maneja la capa de datos. Define qué información es estática y cuál es dinámica.
- SwiftUI (WidgetKit): Se encarga exclusivamente de renderizar la interfaz gráfica de usuario. Al utilizar vistas declarativas, el consumo de batería se mantiene en niveles mínimos.
El corazón de esta arquitectura se basa en el protocolo ActivityAttributes. Este protocolo requiere definir dos estructuras:
- Atributos estáticos: Datos que no cambiarán a lo largo del evento (por ejemplo, el número de pedido o el nombre del conductor).
- ContentState (Estado dinámico): Propiedades que cambian constantemente (como el tiempo estimado de llegada, la puntuación o el estado actual de la entrega).
Casos de uso ideales: ¿Cuándo deberías implementarlas?
No todas las aplicaciones necesitan una Live Activity. Su uso está pensado para eventos transitorios de alto impacto emocional o informativo. Algunos de los casos de uso más destacados incluyen:
- Delivery y Comercio Electrónico: Mostrar las fases de preparación, recolección y trayecto de comida o paquetes en tiempo real.
- Movilidad y Transporte (Ridesharing): Indicar la distancia del vehículo, modelo del coche, matrícula y minutos restantes para la llegada.
- Deportes y Eventos en Vivo: Transmisión de marcadores minuto a minuto, faltas, tarjetas o posesión de balón.
- Productividad y Finanzas: Temporizadores Pomodoro, seguimiento de vuelos comerciales o monitoreo de alertas de criptomonedas y bolsa.
- Fitness y Salud: Ritmo cardíaco actual, distancia recorrida y calorías quemadas durante una sesión de entrenamiento activa.
Anatomía de la Dynamic Island: Diseñando para diferentes estados
Diseñar para la Dynamic Island requiere pensar en microinterfaces. SwiftUI te exige definir la interfaz para cuatro presentaciones distintas:
- Compact Leading (Compacta izquierda): El espacio circular o semicircular ubicado a la izquierda de la cámara frontal (ideal para un icono o indicador de estado).
- Compact Trailing (Compacta derecha): El espacio a la derecha de la cámara (ideal para texto corto, como «5 min» o un porcentaje).
- Minimal (Mínima): Se activa cuando hay más de una Live Activity en ejecución. El sistema decide cuál mostrar como círculo flotante desvinculado de la isla principal.
- Expanded (Expandida): Aparece cuando el usuario mantiene presionada la Dynamic Island. Proporciona un lienzo amplio con cuadrantes superior, inferior y laterales para mostrar botones interactivos y gráficos detallados.
Implementación paso a paso en SwiftUI
Veamos un ejemplo práctico y simplificado de cómo estructurar una Live Activity para el seguimiento de un pedido.
Requisito previo: añade la clave
NSSupportsLiveActivitiescon valorYESalInfo.plistde la app principal. Sin ella,Activity.requestfallará aunque todo lo demás esté bien configurado.
1. Definir los datos con ActivityAttributes
Primero, dentro del target de tu aplicación y de tu extensión de widgets, define el modelo:
import ActivityKit
import Foundation
struct PedidoAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Datos dinámicos
var estado: String
var tiempoRestanteMinutos: Int
}
// Datos estáticos
var numeroPedido: String
var nombreRestaurante: String
}
2. Crear la interfaz con WidgetKit
En la extensión de Widgets, implementas la interfaz visual para la pantalla de bloqueo y la Dynamic Island. Recuerda añadir PedidoLiveActivity() al WidgetBundle marcado con @main de la extensión, o el sistema no sabrá cómo dibujarla:
import WidgetKit
import SwiftUI
struct PedidoLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: PedidoAttributes.self) { context in
// UI para la Pantalla de Bloqueo
VStack(alignment: .leading) {
Text(context.attributes.nombreRestaurante)
.font(.headline)
HStack {
Text(context.state.estado)
Spacer()
Text("\(context.state.tiempoRestanteMinutos) min restantes")
.bold()
}
}
.padding()
} dynamicIsland: { context in
DynamicIsland {
// UI Expandida
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "bag.fill")
}
DynamicIslandExpandedRegion(.trailing) {
Text("\(context.state.tiempoRestanteMinutos) min")
}
DynamicIslandExpandedRegion(.bottom) {
Text("Estado: \(context.state.estado)")
}
} compactLeading: {
// UI Compacta Izquierda
Image(systemName: "bag.fill")
} compactTrailing: {
// UI Compacta Derecha
Text("\(context.state.tiempoRestanteMinutos)m")
} minimal: {
// UI Mínima
Image(systemName: "bag.fill")
}
}
}
}
3. Iniciar la actividad desde la App principal
Desde tu aplicación, cuando el usuario realiza la compra, puedes lanzar la actividad:
import ActivityKit
func iniciarSeguimiento() {
let atributos = PedidoAttributes(numeroPedido: "#1234", nombreRestaurante: "Pizzería Bella")
let estadoInicial = PedidoAttributes.ContentState(estado: "En el horno", tiempoRestanteMinutos: 25)
do {
_ = try Activity.request(
attributes: atributos,
content: .init(state: estadoInicial, staleDate: nil)
)
} catch {
print("Error al iniciar Live Activity: \(error.localizedDescription)")
}
}
4. Actualizar y finalizar la actividad
Mientras la app está en ejecución puedes actualizar el estado o terminar la actividad cuando el pedido llegue:
func actualizarPedido(_ actividad: Activity<PedidoAttributes>) async {
let nuevoEstado = PedidoAttributes.ContentState(estado: "En reparto", tiempoRestanteMinutos: 10)
await actividad.update(.init(state: nuevoEstado, staleDate: nil))
}
func finalizarPedido(_ actividad: Activity<PedidoAttributes>) async {
let estadoFinal = PedidoAttributes.ContentState(estado: "Entregado", tiempoRestanteMinutos: 0)
// .default deja la actividad visible un tiempo en la pantalla de bloqueo; .immediate la quita al momento
await actividad.end(.init(state: estadoFinal, staleDate: nil), dismissalPolicy: .default)
}
Puedes recuperar las actividades en curso en cualquier momento con Activity<PedidoAttributes>.activities.
Actualizaciones remotas: Notificaciones Push (APNs)
Aunque puedes actualizar una Live Activity localmente mientras la app está viva en segundo plano, lo realmente útil es actualizarla desde tu servidor.
ActivityKit permite obtener un Push Token único para cada sesión de actividad iniciada. Tu servidor backend puede enviar cargas útiles (payloads) JSON específicas a través de Apple Push Notification service (APNs) directamente a ese token. Esto actualiza la interfaz de la Dynamic Island y la Pantalla de Bloqueo al instante, sin necesidad de despertar tu aplicación.
Para obtener ese token, inicia la actividad con pushType: .token y escucha sus actualizaciones (el token puede cambiar durante la vida de la actividad):
let actividad = try Activity.request(
attributes: atributos,
content: .init(state: estadoInicial, staleDate: nil),
pushType: .token
)
Task {
for await tokenData in actividad.pushTokenUpdates {
let token = tokenData.map { String(format: "%02x", $0) }.joined()
// Envía el token a tu backend junto con el número de pedido
print("Token de la Live Activity: \(token)")
}
}
Desde el servidor, el envío a APNs usa la cabecera apns-push-type: liveactivity y el topic tu.bundle.id.push-type.liveactivity, con un payload cuyo bloque aps incluye "timestamp" (fecha Unix actual), "event": "update" (o "end" para terminarla) y el nuevo content-state, con las mismas claves que tu ContentState.
Mejores prácticas y limitaciones
Para garantizar una experiencia pulida y evitar que Apple rechace tu implementación en el App Store Review, ten en cuenta las siguientes pautas:
- Duración limitada: Una Live Activity puede permanecer activa por un máximo de 8 horas continuas. Después de este tiempo, pasa a un estado estático y expira completamente a las 12 horas si el usuario no la descarta.
- No abuses de la frecuencia: Aunque admite actualizaciones frecuentes, el sistema limita los pushes de prioridad alta (
apns-priority: 10) para ahorrar batería. Usa prioridad 5 para los cambios no urgentes y, si de verdad necesitas actualizaciones muy frecuentes (por ejemplo, un partido en directo), añadeNSSupportsLiveActivitiesFrequentUpdatesalInfo.plist. - Diseño limpio y legible: Respeta el modo oscuro nativo de la Dynamic Island. Usa colores con alto contraste y evita tipografías diminutas. Puedes comprobar el contraste de tus colores con el conversor de colores y verificador de contraste WCAG.
- Interacciones directas: A partir de iOS 17, puedes integrar botones o toggles con Swift App Intents, permitiendo pausar un temporizador o cancelar una acción sin abrir la app.
Errores típicos que conviene evitar
- Olvidar la clave del Info.plist. Sin
NSSupportsLiveActivities,Activity.requestfalla y el error no siempre es claro. - No registrar el widget. Si
PedidoLiveActivityno está en elWidgetBundle, la actividad se crea pero no se ve. - Diseñar solo la vista expandida. Lo que el usuario ve casi siempre es la vista compacta: cuida que se entienda con un icono y un dato.
- No terminar la actividad. Si no llamas a
end, se queda en pantalla con información vieja hasta que el sistema la retire.
Conclusión
Las Live Activities no sirven para todas las apps, pero cuando tienes un proceso con principio y fin (un pedido, un viaje, un partido, un temporizador) son de lo mejor que puedes ofrecer en iOS.
Empieza por la versión local, sin servidor: con el código de este artículo la tienes funcionando en una tarde. Cuando veas que encaja, añade las actualizaciones por push para que se mantenga al día aunque la app esté cerrada.
Artículos relacionados: Cómo implementar Dynamic Island en apps de productividad con Swift · Widgets Interactivos en SwiftUI: Cómo actualizar datos en tiempo real desde tu Backend · Cómo crear un Widget interactivo para Lock Screen (Pantalla de Bloqueo) en iOS con SwiftUI