Maestría en Live Activities: Integrando Dynamic Island y Pantalla Bloqueada en iOS con Swift y ActivityKit
Domina el desarrollo de Live Activities y Dynamic Island en iOS mediante la implementación de ActivityKit y SwiftUI. Este tutorial completo te guía desde la configuración inicial hasta la sincronización en tiempo real de datos efímeros en pantallas bloqueadas y dispositivos compatibles con Dynamic Island.
🚀 Introducción a las Live Activities y Dynamic Island
Las Live Activities (Actividades en Vivo) representan una de las adiciones más potentes al ecosistema de iOS introducidas en iOS 16 y mejoradas sustancialmente en iOS 17 y 18. Permiten a los usuarios estar al tanto de tareas en curso de larga duración directamente desde la Pantalla Bloqueada y, en dispositivos compatibles, desde la codiciada Dynamic Island.
A diferencia de las notificaciones push tradicionales que desaparecen o se apilan en el centro de notificaciones, una Live Activity proporciona una experiencia interactiva, persistente y visualmente dinámica que se actualiza en tiempo real. Piensa en aplicaciones de entrega de comida, seguimiento de actividad física, marcadores deportivos o control de transporte privado.
🛠️ Requisitos Previos y Configuración del Proyecto
Para seguir este tutorial con éxito, necesitarás cumplir con los siguientes requisitos técnicos:
- Xcode 15 o superior instalado en tu Mac.
- Un dispositivo físico con iOS 16.1 o superior (la Dynamic Island requiere un iPhone 14 Pro o superior, aunque las Live Activities funcionan en la Pantalla Bloqueada de cualquier iPhone compatible con iOS 16.1+).
- Conocimientos básicos de Swift, SwiftUI y Swift Concurrency (
async/await).
Añadiendo el Widget Extension
Las Live Activities se ejecutan dentro de una extensión de Widget (Widget Extension). Para añadir esta extensión a tu proyecto existente:
File > New > Target....Widget Extension bajo la categoría iOS y haz clic en Next.📦 Diseñando el Modelo de Datos con ActivityAttributes
El primer paso para construir una Live Activity es definir la estructura de datos que utilizará. Esta estructura se divide en dos partes:
- Atributos Estáticos (
ActivityAttributes): Datos que no cambian durante toda la vida de la actividad (por ejemplo, el ID del pedido, el nombre del restaurante o el icono principal). - Estado Dinámico (
ContentState): Datos que cambian con frecuencia (por ejemplo, el tiempo estimado de entrega o el estado actual del pedido).
Crea un nuevo archivo llamado DeliveryAttributes.swift y añade el siguiente código:
import Foundation
import ActivityKit
import SwiftUI
struct DeliveryAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Datos que cambiarán en tiempo real
var estimatedMinutes: Int
var deliveryStatus: String
var progress: Double // Valor entre 0.0 y 1.0
}
// Datos estáticos que permanecen constantes
var orderID: String
var restaurantName: String
var itemSummary: String
}
🎨 Creando la Interfaz con SwiftUI
La interfaz de usuario de una Live Activity se construye utilizando SwiftUI. Debes diseñar vistas para tres escenarios principales:
- Pantalla Bloqueada (Lock Screen): Una vista rica y detallada que aparece cuando el usuario enciende su teléfono o desliza hacia abajo.
- Dynamic Island - Vista Compacta (Compact): Pequeñas burbujas a la izquierda y derecha de la cámara frontal.
- Dynamic Island - Vista Expandida (Expanded): Se muestra cuando el usuario mantiene presionada la Dynamic Island.
- Dynamic Island - Vista Mínima (Minimal): Se muestra cuando hay múltiples actividades activas en segundo plano.
Implementando el Widget
Modifica tu archivo de extensión de widget para implementar Widget y configurar las vistas de la siguiente manera:
import WidgetKit
import SwiftUI
@main
struct DeliveryWidgetBundle: WidgetBundle {
var body: some Widget {
DeliveryLiveActivity()
}
}
struct DeliveryLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.swift) { context in
// Diseño para la Pantalla Bloqueada y Banner
LockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Región Expandida
DynamicIslandExpandedRegion(.leading) {
Label(context.attributes.restaurantName, systemImage: "fork.knife")
.font(.headline)
}
DynamicIslandExpandedRegion(.trailing) {
Text("\(context.state.estimatedMinutes) min")
.font(.headline)
.foregroundColor(.orange)
}
DynamicIslandExpandedRegion(.center) {
VStack(alignment: .leading) {
Text(context.state.deliveryStatus)
.font(.subheadline)
ProgressView(value: context.state.progress)
.tint(.orange)
}
}
} compactLeading: {
Label {
Text(context.attributes.restaurantName)
} icon: {
Image(systemName: "bag.fill")
.foregroundColor(.orange)
}
.labelStyle(.iconOnly)
} compactTrailing: {
Text("\(context.state.estimatedMinutes)m")
.font(.caption2)
.bold()
} minimal: {
Image(systemName: "bag.fill")
.foregroundColor(.orange)
}
}
}
}
struct LockScreenView: View {
let context: ActivityViewContext<DeliveryAttributes>
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: "bicycle")
.font(.title2)
.foregroundColor(.orange)
VStack(alignment: .leading) {
Text(context.attributes.restaurantName)
.font(.headline)
Text(context.attributes.itemSummary)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
VStack(alignment: .trailing) {
Text("\(context.state.estimatedMinutes) min")
.font(.title3)
.bold()
.foregroundColor(.orange)
Text(context.state.deliveryStatus)
.font(.caption)
.foregroundColor(.green)
}
}
ProgressView(value: context.state.progress)
.tint(.orange)
}
.padding()
.activityBackgroundTint(Color(.systemBackground))
.activitySystemActionForegroundColor(Color.primary)
}
}
🔄 Gestionando el Ciclo de Vida de la Actividad
Para iniciar, actualizar y finalizar una Live Activity, utilizaremos el framework ActivityKit desde tu aplicación principal (la app iOS normal, no desde la extensión).
1. Iniciar una Live Activity
Crea un servicio o gestor en tu app para controlar las operaciones:
import Foundation
import ActivityKit
class DeliveryManager {
static let shared = DeliveryManager()
private var currentActivity: Activity<DeliveryAttributes>?
func startDelivery(restaurantName: String, summary: String) {
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
print("Las Live Activities no están habilitadas por el usuario.")
return
}
let attributes = DeliveryAttributes(
orderID: UUID().uuidString,
restaurantName: restaurantName,
itemSummary: summary
)
let initialState = DeliveryAttributes.ContentState(
estimatedMinutes: 30,
deliveryStatus: "Pedido confirmado",
progress: 0.1
)
try? Task {
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil),
pushType: nil
)
self.currentActivity = activity
print("Live Activity iniciada con ID: \(activity.id)")
}
}
}
2. Actualizar una Live Activity
Puedes actualizar el estado dinámico llamando al método update(content:) sobre la actividad activa:
func updateDeliveryProgress(minutes: Int, status: String, progress: Double) {
Task {
let updatedState = DeliveryAttributes.ContentState(
estimatedMinutes: minutes,
deliveryStatus: status,
progress: progress
)
await currentActivity?.update(ActivityContent(state: updatedState, staleDate: nil))
print("Live Activity actualizada correctamente.")
}
}
3. Finalizar una Live Activity
Cuando el pedido se complete o se cancele, debes finalizar la actividad. Puedes permitir que el sistema mantenga la notificación visible un tiempo determinado usando una política de desalojo (dismissalPolicy):
func endDelivery() {
Task {
let finalState = DeliveryAttributes.ContentState(
estimatedMinutes: 0,
deliveryStatus: "¡Pedido entregado!",
progress: 1.0
)
await currentActivity?.end(
ActivityContent(state: finalState, staleDate: Date().addingTimeInterval(60 * 5)),
dismissalPolicy: .default
)
currentActivity = nil
print("Live Activity finalizada.")
}
}
🌐 Actualizaciones Remotas con APNs
Aunque las actualizaciones locales funcionan bien si la aplicación está abierta en segundo plano, para escenarios reales de producción (como entregas o marcadores deportivos donde el servidor gestiona el estado), debes utilizar notificaciones push remotas a través del servicio APNs (Apple Push Notification service).
¿Cómo configurar APNs para Live Activities? (Haz clic para expandir)
Para enviar actualizaciones push a una Live Activity:
- Cada Live Activity genera un Push Token único que puedes obtener observando la propiedad
activity.pushTokenUpdates. - Debes enviar este token a tu servidor backend junto con el identificador de la actividad.
- Tu servidor enviará una petición HTTP/2 a APNs con un payload JSON especial que incluye los nuevos valores de
ContentStatey la cabeceraapns-push-type: liveactivity. - El payload de APNs debe usar la clave
apsconteniendo el objetotimestamp,event(update, end) y los datos de estado actualizados.
🧪 Pruebas y Depuración (Debugging)
Probar Live Activities puede ser un desafío debido a la naturaleza en segundo plano de los widgets. Aquí tienes algunas recomendaciones clave:
- Usa un dispositivo real: Aunque el simulador de Xcode soporta Dynamic Island en modelos como el iPhone 15 Pro, la experiencia completa en la Pantalla Bloqueada y gestos táctiles se valida mejor en hardware real.
- Verifica los permisos: Asegúrate de que tu app tiene autorización para mostrar actividades. Puedes verificar esto comprobando
ActivityAuthorizationInfo().areActivitiesEnabled. - Consola de Xcode: Filtra los registros de la consola utilizando la palabra clave
ActivityKitpara ver errores relacionados con tokens, fallos de renderizado o problemas de sincronización.
📊 Resumen de Buenas Prácticas
Para garantizar que tus Live Activities ofrezcan una experiencia de usuario excepcional y cumplan con las directrices de Apple, ten en cuenta esta tabla comparativa:
| Práctica Recomendada 🟢 | Práctica a Evitar 🔴 |
|---|---|
| --- | --- |
| Actualizar solo cuando hay cambios significativos de datos. | Enviar actualizaciones constantes cada pocos segundos sin justificación. |
| Mantener los diseños limpios, legibles y con contraste adecuado. | Sobrecargar la Dynamic Island con demasiados iconos o texto diminuto. |
| --- | --- |
| Finalizar la actividad puntualmente al concluir el evento. | Dejar actividades zombies abiertas indefinidamente en la Pantalla Bloqueada. |
| Proveer un comportamiento robusto ante la pérdida de conexión. | Depender exclusivamente de la app abierta para actualizar el estado. |
🎉 Conclusión
¡Felicidades! Has aprendido los fundamentos y la implementación práctica de las Live Activities y la Dynamic Island en iOS utilizando ActivityKit, SwiftUI y Swift Concurrency. Esta tecnología eleva la interacción del usuario con tu aplicación, ofreciendo información oportuna, útil y visualmente impactante directamente en la pantalla de su dispositivo.
Ahora puedes integrar este conocimiento en tus propios proyectos de iOS para crear experiencias inigualables en tiempo real.
Tutoriales relacionados
- Maestría en Programación Reactiva: Unlocking Combine Framework para iOS y SwiftUIintermediate15 min
- ¡Maestría en Handoff! Continuidad de Actividades en iOS con SwiftUI y UIKitintermediate18 min
- ¡Maestría en Core Data! Persistencia de Datos en iOS con SwiftUI y MVVMintermediate25 min
- Domina Core Animation: Creando Animaciones Impresionantes en iOS con Swiftintermediate20 min
- Maestría en SwiftData: Persistencia de Datos de Próxima Generación en iOS con SwiftUIintermediate20 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!