Para cualquier iOS Developer que busque crear experiencias de usuario inolvidables, mantenerse al día con las últimas capacidades del ecosistema de Apple no es opcional, es una necesidad. Desde su introducción, las Live Activities (Actividades en Vivo) han transformado la forma en que los usuarios interactúan con la información en tiempo real directamente desde la Pantalla de Bloqueo (Lock Screen) y la Dynamic Island.
En este extenso tutorial de programación Swift, exploraremos a fondo qué son, cómo funcionan y, lo más importante, cómo puedes implementar Live Activities en SwiftUI paso a paso utilizando Xcode. Ya sea que estés construyendo tu próxima gran app de entregas, un rastreador de resultados deportivos o un monitor de entrenamiento cruzado entre iOS y watchOS, esta guía te proporcionará los cimientos técnicos necesarios.
1. ¿Qué son las Live Activities?
Las Live Activities son una función del sistema operativo que permite a las aplicaciones mostrar datos en tiempo real y actualizarlos de manera persistente en la Pantalla de Bloqueo y en la Dynamic Island (en los dispositivos compatibles). A diferencia de las notificaciones push tradicionales, que son estáticas y se acumulan en una lista, una Live Activity es un único elemento interactivo (o semi-interactivo) que evoluciona con el tiempo.
Casos de Uso Ideales
- Deportes: Marcadores en vivo de un partido de fútbol.
- Entregas (Delivery): El estado de un pedido (preparando, en camino, entregado).
- Viajes y Transporte: Tiempo de llegada de un Uber o detalles de la puerta de embarque de un vuelo.
- Productividad y Salud: Temporizadores de enfoque (Pomodoro) o el progreso de un entrenamiento en vivo.
Como iOS Developer, adoptar esta característica demuestra un dominio avanzado de SwiftUI y WidgetKit, ya que las Live Activities comparten arquitectura con los widgets de la pantalla de inicio.
2. Requisitos y Configuración Inicial en Xcode
Para comenzar este tutorial de programación Swift, necesitas asegurarte de que tu entorno de desarrollo esté preparado.
Requisitos Técnicos
- IDE: Xcode 14.1 o superior (recomendamos la última versión estable para soporte completo de iOS 17/18).
- Lenguajes: Swift y SwiftUI.
- Target: iOS 16.1 como mínimo (iOS 16.2+ altamente recomendado por los cambios en la API).
Configurando el proyecto en Xcode
- Crea un nuevo proyecto: Abre Xcode y crea un nuevo proyecto iOS tipo “App”. Nómbralo
LiveActivityTutorial. Asegúrate de seleccionar SwiftUI como interfaz. - Modifica el Info.plist: El sistema operativo necesita saber explícitamente que tu app tiene permiso para ejecutar Live Activities.
- Ve al archivo
Info.plistde tu app. - Añade una nueva clave llamada
NSSupportsLiveActivities(o busca Supports Live Activities). - Establece su valor en
YES(Booleano).
- Ve al archivo
- Añade un Widget Extension: Las Live Activities utilizan la infraestructura de WidgetKit para renderizar su interfaz.
- Ve a
File > New > Target... - Selecciona Widget Extension.
- Nómbralo
LiveActivityWidget. - Importante: Asegúrate de marcar la casilla “Include Live Activity” al crear el target. Esto generará el código base que necesitamos.
- Ve a
3. Definiendo el Modelo de Datos: ActivityAttributes
El corazón de cualquier Live Activity en Swift es el protocolo ActivityAttributes. Este protocolo requiere que dividamos nuestros datos en dos categorías estrictas:
- Datos Estáticos: Información que nunca cambiará mientras la actividad esté viva (por ejemplo, el nombre de un restaurante, el número de pedido o los equipos que juegan un partido).
- Datos Dinámicos (Content State): Información que se actualizará con el tiempo (por ejemplo, el estado de la entrega, el marcador actual, el tiempo restante).
Vamos a crear un caso de uso para una aplicación de entrega de pizzas. Crea un nuevo archivo en Swift (asegúrate de que el Target Membership incluya tanto tu App principal como la Widget Extension) llamado PizzaDeliveryAttributes.swift.
import Foundation
import ActivityKit
struct PizzaDeliveryAttributes: ActivityAttributes {
// 1. Datos Dinámicos (El estado que cambia)
public struct ContentState: Codable, Hashable {
var driverName: String
var estimatedDeliveryTime: ClosedRange<Date>
var deliveryStatus: String
}
// 2. Datos Estáticos (Constantes de la actividad)
var orderNumber: String
var pizzaName: String
var restaurantName: String
}
Es crucial mantener el ContentState lo más ligero posible. El sistema de Apple tiene límites estrictos de tamaño para las actualizaciones (generalmente bajo 4KB), por lo que no debes enviar imágenes grandes ni datos innecesarios aquí.
4. Construyendo la Interfaz: Live Activities en SwiftUI
Ahora que tenemos nuestro modelo, vamos al archivo generado por Xcode dentro de nuestra Widget Extension, probablemente llamado LiveActivityWidgetLiveActivity.swift.
Aquí es donde el poder de SwiftUI brilla. Diseñaremos dos vistas principales: una para la Lock Screen y otra para la Dynamic Island.
La Vista de la Lock Screen
La vista de la pantalla de bloqueo suele ser un rectángulo horizontal o una tarjeta (“banner”) que muestra la información de forma expandida.
import ActivityKit
import WidgetKit
import SwiftUI
struct LiveActivityWidgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
// ESTA VISTA ES PARA LA PANTALLA DE BLOQUEO (Lock Screen)
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("🍕 \(context.attributes.restaurantName)")
.font(.headline)
Spacer()
Text("Pedido #\(context.attributes.orderNumber)")
.font(.caption)
.foregroundColor(.gray)
}
Text(context.state.deliveryStatus)
.font(.title2)
.bold()
HStack {
Image(systemName: "car.fill")
Text("Repartidor: \(context.state.driverName)")
Spacer()
// Timer nativo de SwiftUI para rangos de fecha
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.bold()
.foregroundColor(.blue)
}
}
.padding()
// Configurando el color de fondo para que se vea nativo
.activitySystemActionForegroundColor(.white)
.activityBackgroundTint(Color.black.opacity(0.8))
} dynamicIsland: { context in
// CONFIGURACIÓN DE LA DYNAMIC ISLAND
DynamicIsland {
// Expanded UI (Cuando el usuario mantiene presionada la isla)
DynamicIslandExpandedRegion(.leading) {
Text("🍕 \(context.attributes.pizzaName)")
.font(.caption)
}
DynamicIslandExpandedRegion(.trailing) {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.multilineTextAlignment(.trailing)
.font(.caption)
}
DynamicIslandExpandedRegion(.center) {
Text(context.state.deliveryStatus)
.font(.headline)
.padding(.top, 5)
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
Image(systemName: "car.circle.fill")
Text(context.state.driverName)
}
}
} compactLeading: {
// Compact UI (Lado izquierdo de la isla cuando no está expandida)
Text("🍕")
} compactTrailing: {
// Compact UI (Lado derecho de la isla)
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.frame(maxWidth: 25)
} minimal: {
// Minimal UI (Cuando hay múltiples Live Activities compitiendo)
Text("🍕")
}
}
}
}
Análisis del Código de SwiftUI
ActivityConfiguration: Es el bloque constructor del widget. Le indicamos el tipo deActivityAttributesal que va a responder.context.attributes: Accede a los datos estáticos (ej.restaurantName).context.state: Accede a los datos dinámicos actuales (ej.deliveryStatus).Text(timerInterval:countsDown:): Esta es una herramienta maravillosa en SwiftUI. Le pasas un rango de fechas y SwiftUI maneja el contador regresivo de forma automática, actualizando la vista cada segundo sin despertar a tu aplicación, ahorrando batería masivamente.DynamicIsland: Requiere que definamos cuatro vistas:- Expanded: Qué se muestra al hacer un “Long Press”. Debes configurar las regiones (
.leading,.trailing,.center,.bottom). - Compact Leading: Icono o dato a la izquierda del notch dinámico.
- Compact Trailing: Dato a la derecha.
- Minimal: Qué se muestra en forma circular si el usuario tiene dos Live Activities ejecutándose simultáneamente.
- Expanded: Qué se muestra al hacer un “Long Press”. Debes configurar las regiones (
5. El Ciclo de Vida: Iniciar, Actualizar y Finalizar
Con nuestra interfaz lista, debemos volver a nuestra app principal en Xcode. Como iOS Developer, debes manejar con cuidado el ciclo de vida de la actividad mediante el framework ActivityKit.
Crearemos un ViewModel (ObservableObject o el nuevo macro @Observable si usas iOS 17+) para manejar la lógica de negocio.
import Foundation
import ActivityKit
import Combine
class DeliveryViewModel: ObservableObject {
// Almacenamos la referencia a la actividad actual
@Published var currentActivity: Activity<PizzaDeliveryAttributes>?
// MARK: - 1. Iniciar la Live Activity
func startDeliveryActivity() {
// Verificamos si las Live Activities están habilitadas en el dispositivo
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
print("Las Live Activities no están habilitadas por el usuario.")
return
}
// Creamos los datos estáticos
let attributes = PizzaDeliveryAttributes(
orderNumber: "XYZ-987",
pizzaName: "Margarita Familiar",
restaurantName: "Luigi's Pizza"
)
// Creamos el estado inicial dinámico (ej: Entrega en 30 minutos)
let futureDate = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
let initialState = PizzaDeliveryAttributes.ContentState(
driverName: "Mario",
estimatedDeliveryTime: Date()...futureDate,
deliveryStatus: "Preparando tu pizza 👨🍳"
)
// Envolvemos el estado en un ActivityContent (Requerido desde iOS 16.2+)
let content = ActivityContent(state: initialState, staleDate: nil)
do {
// Solicitamos iniciar la actividad
currentActivity = try Activity.request(
attributes: attributes,
content: content,
pushType: nil // Usamos nil porque actualizaremos desde la app. Usa .token si actualizas vía Push Notifications.
)
print("Actividad iniciada con ID: \(currentActivity?.id ?? "")")
} catch {
print("Error al iniciar la Live Activity: \(error.localizedDescription)")
}
}
// MARK: - 2. Actualizar la Live Activity
func updateDeliveryActivity(newStatus: String, newDriver: String? = nil) {
guard let activity = currentActivity else { return }
Task {
// Mantenemos el driver actual si no se proporciona uno nuevo
let currentDriver = activity.content.state.driverName
let estimatedTime = activity.content.state.estimatedDeliveryTime
let updatedState = PizzaDeliveryAttributes.ContentState(
driverName: newDriver ?? currentDriver,
estimatedDeliveryTime: estimatedTime,
deliveryStatus: newStatus
)
let updatedContent = ActivityContent(state: updatedState, staleDate: nil)
// Alert Configuration permite hacer vibrar el dispositivo o expandir la isla dinámicamente al actualizar
var alertConfig = AlertConfiguration(
title: "Actualización de tu pedido",
body: newStatus,
sound: .default
)
await activity.update(updatedContent, alertConfiguration: alertConfig)
print("Actividad actualizada exitosamente.")
}
}
// MARK: - 3. Finalizar la Live Activity
func endDeliveryActivity() {
guard let activity = currentActivity else { return }
Task {
let finalState = PizzaDeliveryAttributes.ContentState(
driverName: activity.content.state.driverName,
estimatedDeliveryTime: activity.content.state.estimatedDeliveryTime,
deliveryStatus: "¡Pizza Entregada! Disfruta. 🍕"
)
let finalContent = ActivityContent(state: finalState, staleDate: nil)
// DismissalPolicy.default deja la notificación en pantalla un rato hasta que el usuario la borre.
// DismissalPolicy.immediate la elimina al instante.
await activity.end(finalContent, dismissalPolicy: .default)
self.currentActivity = nil
print("Actividad finalizada.")
}
}
}
Integración en la Vista Principal de SwiftUI
Finalmente, para activar esta lógica en nuestra app de SwiftUI:
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = DeliveryViewModel()
var body: some View {
VStack(spacing: 20) {
Text("App de Reparto de Pizza")
.font(.largeTitle)
.bold()
Button(action: {
viewModel.startDeliveryActivity()
}) {
Text("Hacer Pedido")
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: {
viewModel.updateDeliveryActivity(newStatus: "En camino 🚗", newDriver: "Luigi")
}) {
Text("Simular: Pedido en Camino")
.frame(maxWidth: .infinity)
.padding()
.background(Color.orange)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: {
viewModel.endDeliveryActivity()
}) {
Text("Simular: Pedido Entregado")
.frame(maxWidth: .infinity)
.padding()
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
}
.padding()
}
}
6. Live Activities en el Ecosistema: iOS, macOS y watchOS
La programación Swift moderna se trata de crear código universal. Sin embargo, como iOS Developer, es fundamental entender dónde y cómo se materializan las Live Activities en diferentes plataformas a través de Xcode.
En iOS y iPadOS
Este es el entorno nativo por excelencia. En el iPhone, las actividades interactúan fluidamente con la Dynamic Island y la Lock Screen. Con iPadOS 17, Apple introdujo las Live Activities en la gran pantalla de bloqueo del iPad, proporcionando un lienzo mucho más amplio. Tu código escrito en SwiftUI para la pantalla de bloqueo funcionará perfectamente aquí, pero debes asegurarte de que tus fuentes y layouts sean escalables.
En watchOS (Smart Stack)
Con watchOS 10 y especialmente watchOS 11, las Live Activities dieron el salto a la muñeca. No necesitas reescribir una aplicación completa de watchOS para que funcionen. El Apple Watch utiliza inteligentemente el código de tu Widget Extension (con un diseño compacto) y proyecta la actividad en vivo directamente en el Smart Stack (pila inteligente) del reloj de tu usuario.
Si el usuario levanta la muñeca, puede ver que su pizza llega en 5 minutos sin necesidad de sacar el iPhone del bolsillo. Para optimizar esto en Xcode, asegúrate de probar tu Widget en el simulador de Apple Watch y evita textos demasiado densos.
En macOS
Actualmente, macOS Sonoma introdujo la capacidad de poner widgets del iPhone en el escritorio de tu Mac gracias a Continuity. Aunque la infraestructura de WidgetKit es compartida, macOS no renderiza “Live Activities” transitorias en un equivalente a la Dynamic Island. Sin embargo, al dominar la arquitectura subyacente de los Widgets con Swift, estarás a un paso de crear widgets estáticos o interactivos de escritorio para macOS usando el mismo modelo mental.
7. Actualizaciones Remotas: El Poder de Apple Push Notification Service (APNs)
En nuestro ejemplo anterior de programación Swift, actualizamos la actividad desde la propia app abierta. Sin embargo, en un entorno de producción, si tu app se cierra o el usuario bloquea el teléfono, la app no puede ejecutar código para actualizar el estado de la entrega.
Para solucionar esto, Apple permite actualizar Live Activities remotamente a través de Notificaciones Push.
- Solicitar un Push Token: Al iniciar la actividad, cambias
pushType: nilapushType: .token. - Enviar el Token al Servidor: Capturas
activity.pushTokenUpdatesusando un bucle asíncrono y envías este token único (que cambia periódicamente) a tu backend (Node.js, Python, Firebase, etc.). - El Backend envía el payload: Tu servidor hace un POST HTTP/2 a los servidores de Apple (APNs) enviando un JSON estructurado que coincide exactamente con los tipos de datos de tu
ContentState. El sistema operativo del iPhone recibe este payload y actualiza la UI de SwiftUI de inmediato, sin despertar a tu aplicación.
8. Mejores Prácticas y Limitaciones a Considerar
Como experto iOS Developer, dominar la técnica no es suficiente; también hay que seguir las Human Interface Guidelines (HIG) de Apple para garantizar que tu app sea aprobada y amada por los usuarios.
Diseño y UX
- Contraste y Color: Usa
activityBackgroundTintpara asegurar que tus textos sean legibles, pero recuerda que el sistema oscurece los colores para mantener la legibilidad de la Lock Screen en modo Always-On Display. - No las uses para publicidad: Apple es estrictísimo con esto. Una Live Activity debe tener un principio y un fin claro relacionado con un evento accionado por el usuario. No la uses para anunciar “¡Tenemos un 20% de descuento hoy!”.
- Imágenes Ligeras: Si necesitas cargar imágenes (como la foto del conductor), pásalas como un dato de tipo
Dataen tuContentStateo cárgalas desde el disco del dispositivo, pero el payload total no puede exceder los 4KB.
Gestión de Energía (Batería)
- Actualiza solo cuando sea necesario: Si el conductor está atascado en el tráfico durante 10 minutos, no envíes una actualización cada segundo cambiando su posición GPS en metros. Actualiza por “eventos” o hitos de cambio de estado.
- Aprovecha el entorno nativo: Usa el temporizador interno de SwiftUI (
Text(timerInterval:)) en lugar de forzar actualizaciones manuales para descontar segundos.
Manejo de Errores en Xcode
- Límites de Actividades Activas: El sistema operativo permite un máximo de actividades ejecutándose al mismo tiempo (generalmente alrededor de 5 por dispositivo y 1 o 2 de una misma app visible a la vez en la isla). Revisa siempre si
ActivityAuthorizationInfo().areActivitiesEnabledes verdadero antes de iniciar tu lógica. - Simulación: Xcode ofrece previsualizaciones (#Preview) excelentes para ver los diferentes estados de la Dynamic Island sin necesidad de compilar todo el tiempo. Úsalas para validar el diseño compacto, expandido y mínimo.
Conclusión
Integrar Live Activities en SwiftUI representa uno de los retornos de inversión más altos en la programación Swift moderna. Proporciona una visibilidad inigualable a tu aplicación directamente desde la pantalla de bloqueo y la Dynamic Island, manteniendo a los usuarios informados e involucrados sin obligarlos a abrir la app constantemente.








