Programación en Swift y SwiftUI para iOS Developers

Notificación Local vs In App vs Push en iOS y SwiftUI

En el ecosistema de desarrollo móvil, la retención de usuarios es tan importante como la adquisición. Para mantener a los usuarios comprometidos, las notificaciones son la herramienta más potente a nuestra disposición. Sin embargo, existe una confusión habitual entre los tres tipos principales de mensajes que podemos mostrar: Notificaciones Push (Remotas)Notificaciones Locales y Notificaciones In-App.

Aunque para el usuario final una “alerta” puede parecer siempre lo mismo, a nivel de arquitectura, código e infraestructura, son bestias completamente diferentes.

Este artículo desglosa cada una, explica sus diferencias técnicas y te enseña a implementarlas utilizando Swift y SwiftUI.


Parte 1: El Panorama General

Antes de escribir una sola línea de código, debemos entender la “fuente de la verdad” de cada notificación.

  1. Notificaciones Locales: El disparador (trigger) es el propio dispositivo (tiempo, fecha o ubicación). No requieren internet.
  2. Notificaciones Push (Remotas): El disparador es un servidor externo. Requieren internet y pasan por los servidores de Apple (APNs).
  3. Notificaciones In-App: El disparador es una acción del usuario dentro de la app o un estado de la interfaz. Son simplemente Vistas de SwiftUI personalizadas.

Parte 2: Notificaciones Locales (Local Notifications)

Las notificaciones locales son programadas por la propia aplicación y entregadas por el dispositivo en el futuro. Son ideales para recordatorios, alarmas o listas de tareas (“To-Do”).+1

Características Clave

  • Independencia: No necesitan un backend.
  • Fiabilidad: Funcionan en modo avión (siempre que el dispositivo esté encendido).
  • Framework: UserNotifications.

Implementación en SwiftUI

Para trabajar con notificaciones locales y remotas de manera limpia en SwiftUI, lo ideal es crear una clase gestora (NotificationManager).

Paso 1: Configurar el Gestor y Pedir Permisos

El sistema operativo iOS requiere que el usuario autorice explícitamente las notificaciones.

import SwiftUI
import UserNotifications

class NotificationManager: NSObject, ObservableObject {
    static let shared = NotificationManager()
    
    override init() {
        super.init()
    }
    
    // 1. Solicitud de Permisos
    func requestAuthorization() {
        let options: UNAuthorizationOptions = [.alert, .sound, .badge]
        
        UNUserNotificationCenter.current().requestAuthorization(options: options) { granted, error in
            if let error = error {
                print("Error al solicitar permisos: \(error.localizedDescription)")
            } else if granted {
                print("Permiso concedido")
            } else {
                print("Permiso denegado")
            }
        }
    }
}

Paso 2: Programar la Notificación

Una notificación local tiene tres partes:

  1. Contenido: ¿Qué dice?
  2. Disparador (Trigger): ¿Cuándo se muestra?
  3. Petición (Request): La combinación de los dos anteriores.
// Dentro de NotificationManager
    
    func scheduleLocalNotification(title: String, body: String, timeInterval: TimeInterval) {
        // A. El Contenido
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        content.sound = .default
        
        // B. El Disparador
        // Se dispara después de X segundos. 'repeats: false' significa que solo ocurre una vez.
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval, repeats: false)
        
        // C. La Petición
        // El identificador debe ser único si quieres gestionar esta notificación específica después
        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
        
        // D. Añadir al Centro de Notificaciones
        UNUserNotificationCenter.current().add(request) { error in
            if let error = error {
                print("Error al programar: \(error.localizedDescription)")
            } else {
                print("Notificación programada para dentro de \(timeInterval) segundos")
            }
        }
    }

Paso 3: Integración en la Vista SwiftUI

struct LocalNotificationView: View {
    @StateObject private var notificationManager = NotificationManager.shared
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Notificaciones Locales")
                .font(.title)
            
            Button("Pedir Permisos") {
                notificationManager.requestAuthorization()
            }
            
            Button("Programar para 5 segundos") {
                notificationManager.scheduleLocalNotification(
                    title: "Hola SwiftUI",
                    body: "Esta es una notificación local",
                    timeInterval: 5
                )
            }
        }
    }
}

Nota importante: Si la app está en primer plano (foreground), por defecto iOS no muestra el banner de la notificación. Para forzarlo, debes implementar el delegado userNotificationCenter(_:willPresent:withCompletionHandler:) en tu NotificationManager.


Parte 3: Notificaciones Push (Remote Notifications)

Aquí es donde la arquitectura se complica. Una notificación Push es un mensaje enviado desde tu servidor a Apple Push Notification service (APNs), y luego de APNs al dispositivo del usuario.

Requisitos Previos

Para probar esto, necesitas:

  1. Una cuenta de Apple Developer (pagada).
  2. Un dispositivo físico (aunque Xcode 14+ permite simulaciones limitadas, lo real es mejor).
  3. Certificados Push o una clave de autenticación .p8 configurada en el portal de desarrolladores.

El Flujo de Trabajo

  1. La App solicita registrarse para notificaciones remotas.
  2. iOS solicita un Device Token a APNs.
  3. La App recibe el token y lo envía a tu servidor backend.
  4. Tu servidor usa ese token para enviar mensajes a ese usuario específico.

Implementación en SwiftUI

SwiftUI no tiene un “AppDelegate” por defecto en el ciclo de vida moderno (WindowGroup), pero lo necesitamos para registrar las Push. Debemos usar UIApplicationDelegateAdaptor.

Paso 1: Crear el AppDelegate Adaptado

class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        
        // Configurar delegado para manejar notificaciones en primer plano
        UNUserNotificationCenter.current().delegate = self
        
        // Solicitar autorización (igual que en locales)
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in }
        
        // Registrarse para remotas
        application.registerForRemoteNotifications()
        
        return true
    }
    
    // ÉXITO al obtener el Token
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // Convertir el token a String hexadecimal
        let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
        let token = tokenParts.joined()
        print("Device Token: \(token)")
        
        // TODO: Enviar este token a tu servidor backend (API)
    }
    
    // ERROR al obtener el Token
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Fallo al registrar notificaciones remotas: \(error)")
    }
}

Paso 2: Conectar al Punto de Entrada de la App

En tu archivo principal (TuApp.swift):

@main
struct TuApp: App {
    // Inyectamos el AppDelegate
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Probando Push sin Backend

Puedes probar una notificación push arrastrando un archivo .apns al simulador de iOS. Crea un archivo llamado payload.apns:

{
    "Simulator Target Bundle": "com.tuempresa.tuapp",
    "aps": {
        "alert": {
            "title": "Prueba Push",
            "body": "Esto viene del servidor (simulado)"
        },
        "sound": "default",
        "badge": 1
    }
}

Arrastra este archivo sobre el simulador y verás la magia.


Parte 4: Notificaciones In-App

Las “In-App Notifications” son las grandes incomprendidas. A menudo, los desarrolladores intentan usar notificaciones locales para mostrar mensajes de éxito (ej. “Perfil guardado”). Esto es un error de UX.

Si el usuario ya está mirando la pantalla, no necesitas el sistema de notificaciones del sistema operativo (que invade el centro de notificaciones). Necesitas una Vista Superpuesta.

Características Clave

  • Control Total: Tú decides el diseño, color, animación y duración.
  • Contexto: Solo existen mientras la app está abierta.
  • Sin Permisos: No requieren permiso del usuario ni certificados de Apple.
  • Tecnología: Vistas estándar de SwiftUI (ZStackoverlaygeometryEffect).

Implementación: Un “Toast” o “Snackbar” Personalizado

Vamos a crear un sistema reutilizable para mostrar notificaciones dentro de la app.

Paso 1: El Modelo y el ViewModel

struct InAppNotification: Equatable {
    var id = UUID()
    var title: String
    var type: NotificationType
    
    enum NotificationType {
        case success
        case error
        case info
        
        var color: Color {
            switch self {
            case .success: return Color.green
            case .error: return Color.red
            case .info: return Color.blue
            }
        }
        
        var icon: String {
            switch self {
            case .success: return "checkmark.circle.fill"
            case .error: return "xmark.circle.fill"
            case .info: return "info.circle.fill"
            }
        }
    }
}

class InAppNotificationManager: ObservableObject {
    @Published var currentNotification: InAppNotification?
    
    func show(title: String, type: InAppNotification.NotificationType) {
        withAnimation(.spring()) {
            currentNotification = InAppNotification(title: title, type: type)
        }
        
        // Ocultar automáticamente después de 3 segundos
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            withAnimation {
                // Solo eliminamos si es la misma notificación (para evitar condiciones de carrera)
                if self.currentNotification?.title == title {
                    self.currentNotification = nil
                }
            }
        }
    }
}

Paso 2: La Vista de la Notificación

struct InAppNotificationView: View {
    let notification: InAppNotification
    
    var body: some View {
        HStack {
            Image(systemName: notification.type.icon)
                .foregroundColor(.white)
            Text(notification.title)
                .foregroundColor(.white)
                .font(.subheadline)
                .fontWeight(.medium)
            Spacer()
        }
        .padding()
        .background(notification.type.color)
        .cornerRadius(10)
        .shadow(radius: 5)
        .padding(.horizontal)
    }
}

Paso 3: El Modificador (Modifier) para Usarlo Fácilmente

Para no ensuciar nuestras vistas con ZStacks por todas partes, creamos un ViewModifier.

struct InAppNotificationModifier: ViewModifier {
    @ObservedObject var manager: InAppNotificationManager
    
    func body(content: Content) -> some View {
        ZStack(alignment: .top) {
            content
            
            if let notification = manager.currentNotification {
                InAppNotificationView(notification: notification)
                    .transition(.move(edge: .top).combined(with: .opacity))
                    .zIndex(1) // Asegura que esté encima
                    .padding(.top, 50) // Espacio para la Dynamic Island o Notch
            }
        }
    }
}

extension View {
    func withInAppNotifications(manager: InAppNotificationManager) -> some View {
        self.modifier(InAppNotificationModifier(manager: manager))
    }
}

Paso 4: Uso Final

struct ContentView: View {
    @StateObject private var inAppManager = InAppNotificationManager()
    
    var body: some View {
        VStack(spacing: 20) {
            Button("Mostrar Éxito") {
                inAppManager.show(title: "Datos guardados correctamente", type: .success)
            }
            
            Button("Mostrar Error") {
                inAppManager.show(title: "Error de conexión", type: .error)
            }
        }
        // Inyectamos el sistema de notificaciones en la raíz
        .withInAppNotifications(manager: inAppManager)
    }
}

Parte 5: Tabla Comparativa y Resumen

Para consolidar el conocimiento, aquí tienes una comparación directa de las tres tecnologías.

CaracterísticaNotificación LocalNotificación PushNotificación In-App
OrigenDispositivo (Código App)Servidor Externo (Backend)Dispositivo (Estado App)
InfraestructuraUserNotifications FrameworkAPNs + Backend + UserNotificationsSwiftUI (Vistas)
Requiere InternetNoNo
Permisos de UsuarioSí (Diálogo de sistema)Sí (Diálogo de sistema)No
VisibilidadFuera de la app (Lock screen/Banner)Fuera de la app (Lock screen/Banner)Solo dentro de la app
CostoGratisRequiere Apple Developer Program (99$/año)Gratis
Caso de Uso PrincipalAlarmas, Recordatorios basados en tiempo/geoNoticias, Chat, Ofertas, Re-engagementFeedback visual, Errores, Confirmaciones

¿Cuándo usar cuál?

  1. Usa Push: Cuando ocurre algo importante fuera del teléfono del usuario que deben saber inmediatamente (ej: “Tu pedido ha sido enviado”, “Nuevo mensaje de Juan”).
  2. Usa Local: Cuando la app debe recordar algo al usuario basado en datos que la app ya tiene, sin necesidad de servidor (ej: “Llevas 3 días sin meditar”, “Tu tarea vence en 10 minutos”).
  3. Usa In-App: Cuando el usuario está interactuando activamente con tu app y necesitas darle feedback inmediato (ej: “Contraseña actualizada”, “Error al cargar datos”). Nunca uses notificaciones Push o Locales para esto, rompe la inmersión.

Conclusión

Dominar las notificaciones en iOS significa entender el contexto del usuario.

  • El código para Push es complejo por la infraestructura (Certificados, APNs).
  • El código para Local es verboso por la configuración de Triggers.
  • El código para In-App es creativo y depende puramente de tus habilidades de diseño en SwiftUI.

Como desarrollador Swift, tu objetivo no es solo “hacer que suene el teléfono”, sino elegir el canal adecuado para que el usuario agradezca la información en lugar de desinstalar la app por molesta.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Cómo crear una aplicación basada en documentos en SwiftUI

Next Article

Cómo instalar GitHub Copilot en Xcode para Swift y SwiftUI

Related Posts