wandres.dev
NOTIFICACIONES · locales y push

Permisos: el momento, los niveles y el no

El diálogo de autorización de notificaciones aparece una sola vez en la vida de una instalación, y lo que ocurre en ese instante condiciona todo lo demás. Esta lección analiza el permiso como un protocolo de negociación con tres variables acopladas —cuándo se pregunta, con qué nivel se pregunta y qué se hace con la negativa—, desmonta los cuatro estados de autorización que reconoce `UNUserNotificationCenter` y propone una arquitectura que no dependa del sí.

⏱ 18 min

Pedir permiso para notificar es la decisión más irreversible que toma una app en su primer minuto de vida. El sistema presenta el diálogo de UNUserNotificationCenter exactamente una vez por instalación; a partir de ahí, cualquier segunda oportunidad exige que la persona navegue voluntariamente hasta Ajustes, cosa que prácticamente nadie hace. Conviene por tanto dejar de tratar la autorización como una llamada de API con dos resultados y empezar a tratarla como lo que es: un protocolo de negociación irrepetible en el que se juegan tres variables acopladas —el momento, el nivel y la arquitectura del rechazo—, y en el que la única estrategia sólida consiste en diseñar la app para que funcione bien incluso cuando la respuesta sea que no.

🎯 Al terminar esta lección sabrás
  • Diseñar la secuencia de cebado previa al diálogo del sistema y justificar su momento con señales de uso reales.
  • Distinguir autorización explícita, provisional, efímera y crítica, y elegir la adecuada según el tipo de producto.
  • Leer y ramificar sobre authorizationStatus y sobre los ajustes finos que la persona puede desactivar por separado.
  • Construir una degradación funcional para el estado denegado que no dependa de reconquistar el permiso.

El diálogo que solo aparece una vez

La llamada a requestAuthorization tiene una propiedad que la separa de casi cualquier otra API del sistema: no es idempotente en su efecto visible. La primera invocación presenta el diálogo modal y registra la respuesta de forma permanente; las siguientes devuelven en silencio el valor almacenado sin mostrar nada. Esto significa que el momento de esa primera llamada no es un detalle de implementación sino una decisión de producto con consecuencias que se arrastran durante toda la vida de la instalación.

El antipatrón dominante consiste en invocarla desde application(_:didFinishLaunchingWithOptions:) o desde el task de la vista raíz. En ese instante la persona no tiene absolutamente ningún contexto: acaba de abrir algo que no conoce, no ha visto valor todavía y se le pide un canal de interrupción permanente. Las tasas de aceptación en ese escenario se hunden, y lo peor es que el fracaso es definitivo. La alternativa correcta es el cebado: una pantalla propia, controlada por ti, que explica qué se va a notificar y por qué merece la pena, y que solo dispara el diálogo del sistema si la persona pulsa el botón afirmativo. La pantalla de cebado es reintentable; el diálogo del sistema no lo es.

enum Cebado {
    static func pedirSiProcede() async -> Bool {
        let centro = UNUserNotificationCenter.current()
        let ajustes = await centro.notificationSettings()
        guard ajustes.authorizationStatus == .notDetermined else {
            return ajustes.authorizationStatus == .authorized
        }
        return (try? await centro.requestAuthorization(
            options: [.alert, .sound, .badge, .providesAppNotificationSettings]
        )) ?? false
    }
}

El criterio para elegir el momento es sencillo de enunciar y difícil de aplicar: se pide el permiso justo después de que la persona haya ejecutado una acción cuyo valor futuro dependa de una notificación. Ha creado un recordatorio, ha activado el seguimiento de un envío, ha entrado en una conversación, ha puesto una alerta de precio. En ese punto la pregunta no es una intromisión sino la continuación natural de lo que acaba de hacer, y la aceptación se multiplica porque el beneficio es concreto en lugar de abstracto.

⚠️
La opción providesAppNotificationSettings no es decorativa

Incluir providesAppNotificationSettings entre las opciones hace que iOS añada un enlace directo a tu propia pantalla de preferencias dentro de los ajustes de notificaciones del sistema. Sin esa opción, la persona que quiere recibir menos avisos solo tiene un interruptor disponible: apagarlo todo. Con ella, existe una ruta hacia la granularidad que tú controlas, y la desactivación total deja de ser la única salida. Es una línea de código que convierte un acantilado en una pendiente.

Los cuatro niveles de autorización

La autorización no es binaria. UNAuthorizationOptions y los estados resultantes describen un espacio con cuatro regímenes distintos, y elegir mal el régimen es tan costoso como elegir mal el momento.

🔔

Explícita

El diálogo clásico. La persona decide con información delante. Es el único nivel que habilita alertas en pantalla bloqueada, sonido y globo numérico desde el primer envío.

🌙

Provisional

Sin diálogo. Las notificaciones se entregan silenciosas al centro de notificaciones y cada una ofrece conservar o desactivar. Cambia la pregunta previa por una demostración.

Efímera

Exclusiva de App Clips. Vive lo que vive el clip y desaparece con él. No se solicita: la concede el sistema.

🚨

Crítica

Atraviesa el modo silencio y los modos de concentración. Requiere una autorización expresa de Apple sobre la cuenta de desarrollo y solo se justifica en salud, seguridad y protección.

La autorización provisional merece un análisis aparte porque invierte por completo el orden del argumento. En lugar de pedir confianza antes de haber demostrado nada, entrega notificaciones silenciosas —sin sonido, sin pantalla bloqueada, directas al centro de notificaciones— y adjunta a cada una los botones de conservar o desactivar. La persona juzga sobre ejemplos reales en lugar de sobre una promesa. Es la opción correcta cuando el valor de tus avisos solo se entiende viéndolos, y su coste es la invisibilidad inicial: si tu producto depende de que el primer aviso interrumpa, lo provisional no sirve.

Un eje ortogonal a la autorización es el nivel de interrupción de cada notificación concreta, que se declara por mensaje y no por app: passive para lo que puede esperar al próximo vistazo, active como valor por defecto, timeSensitive para lo que debe atravesar un modo de concentración, y critical para lo que debe atravesar incluso el silencio. Los dos últimos exigen derechos especiales en el perfil de aprovisionamiento, y su uso indebido es una causa habitual de rechazo en la revisión de la App Store.

Leer el estado, no asumirlo

El error de arquitectura más extendido es guardar en UserDefaults un booleano con la respuesta del usuario y consultarlo después. Ese valor caduca sin avisar: la persona puede revocar el permiso desde Ajustes, puede activar la entrega programada de resúmenes, puede apagar solo el sonido, puede desactivar la pantalla bloqueada y dejar el resto. La fuente de verdad es siempre notificationSettings, y hay que releerla en cada retorno a primer plano.

@MainActor
final class EstadoNotificaciones: ObservableObject {
    @Published private(set) var puedeAlertar = false
    @Published private(set) var esProvisional = false
    @Published private(set) var suenaEnPantallaBloqueada = false

    func refrescar() async {
        let a = await UNUserNotificationCenter.current().notificationSettings()
        puedeAlertar = a.alertSetting == .enabled
        esProvisional = a.authorizationStatus == .provisional
        suenaEnPantallaBloqueada =
            a.soundSetting == .enabled && a.lockScreenSetting == .enabled
    }
}

Los ajustes finos importan porque describen capacidades reales, no intenciones. Una app puede estar autorizada y aun así tener el sonido apagado, el globo numérico desactivado y la entrega diferida a un resumen programado dos veces al día. Programar un recordatorio de medicación asumiendo que sonará, cuando soundSetting está deshabilitado, produce un fallo silencioso que ningún registro de errores capturará jamás.

Hay además una lectura estratégica de estos campos que rara vez se aprovecha. La transición de un usuario desde el estado autorizado con sonido hasta el estado autorizado sin sonido y sin pantalla bloqueada es una señal temprana e inequívoca de fatiga: esa persona está a un paso de la desactivación total y todavía se puede recuperar bajando la frecuencia. Observar esa degradación por cohortes es un indicador adelantado mucho más fino que la tasa de permiso concedido, que solo se mueve cuando ya es tarde.

flowchart TB
a[Accion con valor futuro] --> b{Estado de autorizacion}
b -->|notDetermined| c[Pantalla de cebado propia]
c -->|acepta| d[Dialogo del sistema]
c -->|pospone| e[Reintentable mas tarde]
d -->|concede| f[Leer ajustes finos]
d -->|deniega| g[Ruta degradada permanente]
b -->|provisional| h[Entrega silenciosa y promocion]
b -->|denied| g
f --> i[Programar segun capacidades reales]

Arquitectura del no

Un tercio de las instalaciones dirá que no y no cambiará de opinión. Diseñar bajo el supuesto contrario produce apps que se rompen para esa población de forma invisible. La disciplina correcta consiste en tratar la notificación como un canal redundante y nunca como el único portador de una información necesaria: todo lo que se anuncia por notificación debe existir también dentro de la app, con marca de no leído, ordenado por recencia y accesible sin depender de que un aviso llegara.

Cuando la persona ha denegado, hay exactamente una acción legítima: ofrecer, en el punto donde la carencia se nota, un enlace a los ajustes del sistema mediante UIApplication.openSettingsURLString, acompañado de una explicación de qué se está perdiendo. No un modal al arrancar, no un recordatorio semanal, no una insistencia: un enlace contextual en el lugar exacto donde la funcionalidad se degrada. Insistir fuera de contexto empeora la métrica que se pretende mejorar, porque el siguiente paso del usuario molesto no es conceder el permiso sino desinstalar.

struct AvisoDegradado: View {
    let estado: EstadoNotificaciones

    var body: some View {
        if !estado.puedeAlertar {
            ContentUnavailableView {
                Label("Sin avisos de entrega", systemImage: "bell.slash")
            } description: {
                Text("Los cambios seguirán apareciendo aquí con marca de no leído.")
            } actions: {
                Button("Activar en Ajustes") {
                    if let url = URL(string: UIApplication.openSettingsURLString) {
                        UIApplication.shared.open(url)
                    }
                }
            }
        }
    }
}

Conviene además auditar el catálogo de funcionalidades bajo esta óptica antes de escribir código. Para cada aviso que la app envía, la pregunta es qué sucede con esa información si la notificación nunca llega: si la respuesta es que se pierde, no estás ante un problema de permisos sino ante un defecto de persistencia. La notificación puede acelerar el conocimiento de un hecho, pero nunca debe ser el único lugar donde ese hecho existe.

El permiso es un contrato, y el diálogo es solo su firma

Hay una asimetría fundamental en el modelo de notificaciones que casi nadie enuncia con claridad: la app pide un derecho de interrupción ilimitado en el tiempo a cambio de una promesa no verificable. El sistema operativo, consciente de esa asimetría, ha ido acumulando durante una década mecanismos de reequilibrio —modos de concentración, resúmenes programados, niveles de interrupción, autorización provisional, ajustes por app dentro del propio sistema— que en conjunto constituyen una devolución progresiva del poder al usuario. Leer esa historia como una serie de obstáculos técnicos es el error de perspectiva que produce apps hostiles. La lectura correcta es que la plataforma está codificando en API una norma social: la atención ajena es un recurso escaso, prestado y revocable. De ahí se sigue algo que reordena las prioridades de ingeniería. El objetivo del diseño de permisos no es maximizar la tasa de concesión, porque un permiso obtenido por presión produce silenciamientos, desactivaciones y desinstalaciones que ninguna métrica de opt-in registra. El objetivo es maximizar la proporción de notificaciones que la persona habría pedido si le hubieran preguntado por cada una. Un equipo que optimiza la primera magnitud construye un embudo; un equipo que optimiza la segunda construye un canal que sobrevive años. Y el corolario práctico es incómodo pero liberador: si tu app necesita el permiso para funcionar, tu app está mal diseñada. El permiso debe añadir oportunidad, no rescatar una carencia.

📝
Lo esencial

El diálogo del sistema aparece una vez y su respuesta es permanente: precédelo siempre de una pantalla de cebado propia, disparada tras una acción cuyo valor dependa de un aviso futuro. Elige el nivel con criterio —explícito cuando la interrupción importa desde el primer mensaje, provisional cuando el valor solo se entiende viéndolo— e incluye providesAppNotificationSettings para que la granularidad exista. No caches la respuesta: relee notificationSettings en cada vuelta a primer plano y ramifica sobre capacidades reales. Y diseña la app para que el no sea una ruta funcional completa, no un estado degradado.

⚔️ Auditar la negociación completa del permiso
  1. Localiza en tu código la llamada a requestAuthorization y documenta qué sabe la persona sobre tu producto en ese instante exacto; si la respuesta es nada, has encontrado el problema.
  2. Construye una pantalla de cebado con un botón afirmativo y otro de posponer, e instrúmenta ambos para medir la conversión hacia el diálogo real y desde él.
  3. Sustituye cualquier booleano cacheado en UserDefaults por una lectura de notificationSettings en el retorno a primer plano, y ramifica sobre soundSetting y lockScreenSetting por separado.
  4. Elige una funcionalidad que hoy dependa exclusivamente de una notificación y dótala de un equivalente dentro de la app con marca de no leído.
  5. Prueba el flujo completo con autorización provisional y compara, sobre una cohorte real, la retención del canal a treinta días frente a la solicitud explícita.