tutoriales.com

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.

Avanzado12 min de lectura10 views
Reportar error

🚀 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.

💡 Consejo: Las Live Activities consumen recursos de energía del sistema. Apple diseña este framework para ser altamente eficiente, por lo que debes actualizar los datos con moderación y utilizar mecanismos adecuados como Push Notifications para actualizaciones remotas o ActivityKit para actualizaciones locales.

🛠️ 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:

Paso 1: Abre tu proyecto en Xcode y ve al menú superior File > New > Target....
Paso 2: Selecciona la plantilla Widget Extension bajo la categoría iOS y haz clic en Next.
Paso 3: Dale un nombre descriptivo a tu extensión (por ejemplo, DeliveryWidget) y asegúrate de marcar la casilla que dice Include Live Activity. Haz clic en Finish.

📦 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:

  1. 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).
  2. 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
}
DeliveryAttributes Protocolo ActivityAttributes Atributos Estáticos ID pedido, Número de ítems ContentState (Dinámico) Estado, ETA, Ubicación Pantalla de Bloqueo Widget de Live Activity (Vista única enriquecida) Dynamic Island Compacta Mínima Expandida

🎨 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)
    }
}
⚠️ Advertencia: Las vistas de las Live Activities tienen restricciones estrictas de rendimiento y memoria. Evita animaciones complejas infinitas, uso excesivo de imágenes pesadas y llamadas de red dentro de las vistas de SwiftUI del widget.

🔄 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 ContentState y la cabecera apns-push-type: liveactivity.
  • El payload de APNs debe usar la clave aps conteniendo el objeto timestamp, 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 ActivityKit para 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

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!