Si vienes del mundo de UIKit (AppDelegate y SceneDelegate) o si estás empezando desde cero con SwiftUI, entender el Ciclo de Vida (Lifecycle) de una aplicación es el cimiento más importante para crear apps robustas.
El ciclo de vida dicta cómo tu aplicación nace, cómo vive mientras el usuario interactúa con ella, y cómo muere (o se suspende) cuando el usuario la abandona. En SwiftUI, Apple ha unificado este proceso, alejándose de la delegación imperativa hacia un enfoque declarativo y reactivo.
En este tutorial, desglosaremos cada fase del ciclo de vida en el ecosistema moderno de Apple, cubriendo las diferencias críticas entre iPhone, Mac y Apple Watch.
1. La Evolución: De la Delegación a la Declaración
Antes de SwiftUI 2.0, el punto de entrada de una app en iOS era el archivo AppDelegate.swift. Este archivo era un “cajón de sastre” donde manejábamos la configuración inicial, las notificaciones push, y los estados de fondo.
Con la llegada del protocolo App en SwiftUI, Apple introdujo un nuevo punto de entrada:
@main
struct MiSuperApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}¿Qué está pasando aquí?
@main: Esta anotación le dice al compilador: “Aquí es donde comienza todo”. Reemplaza al antiguo archivomain.swifto la anotación@UIApplicationMain.AppProtocol: Tu estructura conforma el protocoloApp. Su único requerimiento es una propiedad computadabodyque devuelva unaScene.SceneyWindowGroup: Una aplicación no contiene vistas directamente; contiene Escenas. Una escena es un contenedor para la interfaz de usuario.WindowGroupes la escena más común, capaz de manejar múltiples ventanas automáticamente (crucial en iPadOS y macOS).
2. El Monitor de Signos Vitales: scenePhase
En el antiguo UIKit, escuchábamos métodos como applicationDidEnterBackground. En SwiftUI, el sistema nos “inyecta” el estado actual a través de una variable de entorno llamada scenePhase.
Esta es la herramienta más poderosa en tu arsenal. Permite a tu app reaccionar a cambios de estado en tiempo real.
Implementación Básica
Para observar el ciclo de vida, necesitas leer la variable de entorno en tu estructura App:
import SwiftUI
@main
struct CicloDeVidaApp: App {
// 1. Inyectamos la variable de entorno
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
// 2. Escuchamos los cambios
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .active:
print("🟢 La app está activa y respondiendo")
case .inactive:
print("🟡 La app es visible pero no interactiva")
case .background:
print("🔴 La app está en segundo plano")
@unknown default:
print("⚪️ Estado futuro desconocido")
}
}
}
}3. Desglose Profundo de los Estados
Entender cuándo se dispara cada estado es vital para evitar bugs de pérdida de datos o consumo excesivo de batería.
A. Active (Activa)
El estado “feliz”. Tu aplicación está en primer plano, ocupa la pantalla (o una ventana) y está recibiendo eventos del usuario (toques, clics, teclado).
- Qué hacer aquí:
- Reanudar animaciones.
- Iniciar temporizadores (timers).
- Solicitar actualizaciones de ubicación o giroscopio.
- Obtener datos frescos de la red si es necesario.
B. Inactive (Inactiva)
Este es un estado transitorio o de “limbo”. La aplicación sigue siendo visible en la pantalla, pero no está recibiendo eventos táctiles.
- ¿Cuándo ocurre esto?
- iOS: Cuando bajas la cortina de notificaciones o abres el Centro de Control.
- iOS: Cuando aparece una llamada entrante o una alerta del sistema de alta prioridad (como la petición de FaceID).
- iPadOS: En multitarea, cuando el usuario está redimensionando ventanas.
- macOS: Cuando la ventana no tiene el foco principal.
- Qué hacer aquí:
- Pausar juegos o lógica pesada.
- Desenfocar contenido sensible (como en apps bancarias) por seguridad visual.
C. Background (Segundo Plano)
El usuario ha cerrado la app (deslizando hacia arriba) o ha cambiado a otra aplicación. La interfaz no es visible.
Nota Crítica: En este punto tienes muy poco tiempo (segundos) antes de que el sistema suspenda la ejecución de tu código para ahorrar batería.
- Qué hacer aquí (¡Rápido!):
- Guardar datos: Persistir cambios en CoreData, UserDefaults o archivos. Nunca esperes a que la app se cierre para guardar.
- Cancelar tareas de red: Si estás descargando una imagen grande que no es vital, cancélala.
- Limpiar recursos: Liberar memoria si es posible.
4. Diferencias entre Plataformas
Aunque SwiftUI intenta ser universal, el comportamiento del usuario cambia drásticamente entre un reloj, un teléfono y un ordenador.
iOS y iPadOS
El comportamiento es el estándar descrito arriba. Sin embargo, recuerda que en iPadOS (con Stage Manager), un usuario puede tener múltiples escenas de tu app abiertas.
- Reto: Una ventana puede estar
.activemientras otra está.background.scenePhaserastrea la escena, no toda la aplicación.
macOS
El ciclo de vida en Mac es más complejo debido a la naturaleza de las ventanas y la barra de menú.
- Cierre de Ventana vs. Cierre de App: En Mac, cerrar la última ventana (botón rojo X) no siempre cierra la aplicación. La app sigue corriendo en el Dock.
NSApplicationDelegateAdaptor: A menudo necesitarás esto para manejar menús superiores o comportamientos específicos del Dock.
Para forzar que la app muera al cerrar la última ventana (común en herramientas simples), no usas scenePhase, sino un método en el delegado o lógica de ventana, aunque SwiftUI está mejorando esto.
watchOS
El Apple Watch es agresivo con la batería.
- Frontmost App: Las apps en watchOS pasan a
inactivemuy rápido cuando el usuario baja la muñeca. - Always On Display: Si tu app soporta “Siempre activa”, el ciclo de vida tiene matices. La app puede parecer activa pero estar en un modo de baja actualización (1Hz).
- Snapshotting: El sistema toma una “foto” de tu UI antes de mandarla al fondo para usarla en el Dock. Asegúrate de que la UI se vea bien antes de pasar a
background.
5. El Puente al Pasado: AppDelegate en SwiftUI
A veces, el ciclo de vida puro de SwiftUI no es suficiente. Casos comunes:
- Configuración de Firebase al inicio.
- Manejo de Notificaciones Push (registro de tokens).
- Manejo de Deep Links complejos.
Para esto, SwiftUI nos ofrece adaptadores para inyectar el antiguo delegado.
En iOS (UIApplicationDelegateAdaptor)
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("🚀 AppDelegate: La app ha arrancado (código legacy/libs)")
// Configurar Firebase aquí
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// Manejo de tokens de push
}
}
@main
struct MiAppHibrida: App {
// Conectamos el delegado
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Ojo: SwiftUI gestiona la creación de la Window, así que no intentes crear UIWindow manualmente dentro del AppDelegate a menos que sepas exactamente por qué lo haces.
6. Arquitectura: ¿Dónde pongo la lógica?
Un error común de los principiantes es poner la lógica de guardado directamente dentro del struct App o dentro de la Vista. Esto rompe el principio de separación de responsabilidades.
El Enfoque MVVM (Model-View-ViewModel)
Lo ideal es que tu ViewModel o un Service manejen la reacción al cambio de estado.
Ejemplo de Arquitectura Limpia:
class DataManager: ObservableObject {
func saveData() {
print("💾 Guardando datos en disco...")
}
func refreshData() {
print("🔄 Refrescando datos de la API...")
}
}
struct ContentView: View {
@Environment(\.scenePhase) var scenePhase
@StateObject var dataManager = DataManager()
var body: some View {
ListaDeTareas(manager: dataManager)
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .active:
// Al volver, refrescamos por si hubo cambios en la nube
dataManager.refreshData()
case .background:
// Al salir, guardamos inmediatamente
dataManager.saveData()
default: break
}
}
}
}¿Por qué en la Vista y no en App?
A veces es mejor poner el .onChange en la vista raíz (ContentView) en lugar del archivo App. Esto se debe a que @StateObjectvive dentro de la jerarquía de vistas. Si tu gestor de datos es un StateObject instanciado en la vista, es más fácil acceder a él desde ahí.
7. Casos de Uso Avanzados y “Gotchas”
A. Tareas en Segundo Plano (Background Tasks)
Si necesitas más tiempo que los pocos segundos que te da el estado background (por ejemplo, para terminar de subir un archivo), necesitas usar el framework BackgroundTasks. El ciclo de vida de SwiftUI no te mantiene vivo indefinidamente por sí solo.
B. Inicialización (init)
El init() de tu estructura App se ejecuta antes que casi cualquier otra cosa.
- Bien: Configurar inyección de dependencias ligera.
- Mal: Tareas pesadas o asíncronas. La UI aún no existe.
C. Persistencia de Escena (SceneStorage)
SwiftUI ofrece @SceneStorage, que es como UserDefaults pero específico para cada ventana. Si tienes dos ventanas abiertas de la misma app en iPad, y matas la app, al restaurarla @SceneStorage recordará qué pestaña tenías abierta en cadaventana individualmente. Esto es parte integral del ciclo de vida de restauración de estado.
Conclusión
El ciclo de vida en SwiftUI ha simplificado drásticamente la forma en que pensamos sobre los estados de nuestra aplicación. Ya no tenemos que preocuparnos por métodos delegados dispersos; ahora tenemos un flujo de estado reactivo y centralizado.
Resumen de supervivencia:
- Usa
@mainy el protocoloApp. - Vigila
@Environment(\.scenePhase)para detectar cambios. - Usa
.activepara arrancar motores y.backgroundpara frenar y guardar. - No temas usar
UIApplicationDelegateAdaptorsi necesitas librerías antiguas.
Dominar scenePhase es la diferencia entre una app que consume batería y pierde datos, y una app que se siente nativa, ágil y fiable en cualquier plataforma de Apple.
Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram










