Notificaciones en iOS
Al terminar: Programar una semana de avisos locales con contenido distinto y reponerlos al volver a la app.
En Android hay un trabajo que se despierta cada día y decide qué enseñar. En iOS eso no existe, y la lección es entender por qué no hace falta.
Aquí se programan siete notificaciones de golpe, una por día, cada una con su contenido ya escrito. Funciona porque el Pokémon del día es una función de la fecha: se puede calcular el de dentro de seis días sin red y sin servidor. Con un contenido que dependiera de una API, esto no se podría hacer, y habría que pasar por notificaciones push (avisos que no salen del móvil, sino que los envía un servidor por internet, el ordenador de la empresa que está siempre encendido) y por montar ese servidor.
La lección termina con el aviso llegando al móvil. Tocarlo todavía no abre la ficha del Pokémon: de eso se ocupa la siguiente, Del aviso a la ficha, en iOS, donde está la sorpresa del módulo (el toque no llega por .onOpenURL).
Los pasos de Kotlin se hacen en Android Studio y los de Swift, en Xcode.
No se instala ninguna librería ni paquete: UserNotifications viene con iOS y kotlinx-datetime ya está en shared desde El Pokémon del día en el núcleo.
Lo nuevo, con un ejemplo ajeno al proyecto
Para programar avisos con fecha aparecen cuatro ideas. Primero, con palabras, usando una app de plantas que avisa de regar:
Datees un instante en el tiempo, sin más: no sabe de meses ni de husos horarios.Calendarsabe leer un instante como lo lee una persona: «16 de octubre, a las 8, en la zona del usuario».Calendar.currentes el calendario del móvil. Sabe sumar días a una fecha y partirla en trozos.DateComponentses una fecha en piezas sueltas (año, mes, día, hora, minuto…) que puedes rellenar o cambiar una a una, como las casillas de un formulario.- Un disparador (trigger) dice cuándo suena un aviso.
UNCalendarNotificationTriggersuena cuando el reloj coincide con unas piezas (por ejemplo, el 16 de octubre a las 8:00) yUNTimeIntervalNotificationTriggersuena pasados unos segundos desde ahora. Conrepeats: falsesuena una vez; conrepeats: true, cada vez que el reloj vuelva a coincidir.
Además, un aviso puede llevar datos ocultos: su userInfo es un diccionario (pares clave → valor) que viaja escondido dentro del aviso. El usuario no lo ve; la app lo lee cuando el aviso se toca. En la próxima lección se usa para el enlace a la ficha.
Un aviso por cada uno de los próximos tres días, a las 8:00. Pégalo en tu Playground (reutiliza el mismo y borra lo anterior, como en Notificaciones locales). Es Swift de consola: no envía nada, solo construye las peticiones y las imprime. Los ! del final de algunas líneas significan «seguro que no es nil» (Opcionales); en el proyecto se usará guard let, que es más prudente. print escribe en la consola de Xcode lo que le des, separado por espacios.
import Foundation
import UserNotifications
// Una app de plantas: avisa de regar los tres próximos días a las 8:00.
let calendario = Calendar.current // el calendario del usuario (zona y reglas)
var hoy = DateComponents() // una fecha fija para que la salida sea siempre igual
hoy.year = 2026
hoy.month = 10
hoy.day = 16
let inicio = calendario.date(from: hoy)! // de piezas sueltas a una fecha: 16 oct 2026, 00:00
for dia in 0..<3 { // 0..<3 son los números 0, 1 y 2
// «inicio + dia días»
let fecha = calendario.date(byAdding: .day, value: dia, to: inicio)!
// De la fecha a sus piezas: año, mes y día (`.day` es la pieza «día del mes»,
// y `[.year, .month, .day]` es la lista de piezas que se piden); después se le pone la hora
var cuando = calendario.dateComponents([.year, .month, .day], from: fecha)
cuando.hour = 8
cuando.minute = 0
// El contenido se escribe AHORA: el día que suene dirá esto, tal cual.
let contenido = UNMutableNotificationContent()
contenido.title = "Toca regar"
contenido.body = "Día \(cuando.day!): el helecho"
contenido.userInfo = ["planta": "helecho"] // datos ocultos que viajan con el aviso
// Suena una sola vez, cuando el reloj coincida con esas piezas.
let disparo = UNCalendarNotificationTrigger(dateMatching: cuando, repeats: false)
let peticion = UNNotificationRequest(identifier: "riego-\(dia)", content: contenido, trigger: disparo)
print(peticion.identifier, "·", contenido.body, "·", cuando.day!, "/", cuando.month!, "a las", cuando.hour!, "·", contenido.userInfo["planta"]!)
}Debe imprimir:
riego-0 · Día 16: el helecho · 16 / 10 a las 8 · helecho
riego-1 · Día 17: el helecho · 17 / 10 a las 8 · helecho
riego-2 · Día 18: el helecho · 18 / 10 a las 8 · helechoTres cosas para quedarse con ellas. La primera: el texto de cada aviso está escrito ya, el 16, aunque suene el 18. La segunda: cada petición lleva un identifier (su nombre); si añades otra con el mismo nombre, sustituye a la que había en vez de sumarse. La tercera: el sistema no guarda avisos sin límite.
Lo importante de esa frase es el final: descarta el resto. No hay error, no hay excepción y no hay aviso. Si programaras noventa días, 26 se perderían sin decir nada y lo descubrirías tres meses después. Cuáles se pierden no está garantizado, así que la única regla segura es no acercarse a 64.
Paso 1 · Un puente de tres enteros
El archivo. DailyIos.kt es una función corta en iosMain que le da a Swift el Pokémon de una fecha escrita como tres números.
Créalo. En Android Studio, con la vista Project: clic derecho sobre el nodo shared → New → File y la ruta desde la carpeta del módulo (el método de El módulo compartido y la prueba de humo); la ruta, desde la carpeta del módulo, es:
src/iosMain/kotlin/com/sebastiannarvaez/pokedex/feature/daily/DailyIos.ktPlan B, por la Terminal: ábrela en la carpeta de tu proyecto (la que contiene shared e iosApp) y escribe mkdir -p shared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/feature/daily (crea todas las carpetas del camino que falten) y después touch shared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/feature/daily/DailyIos.kt (crea el fichero vacío).
package com.sebastiannarvaez.pokedex.feature.daily
import kotlinx.datetime.LocalDate
/**
* El Pokémon del día a partir de tres enteros.
* Swift lo ve como `DailyIosKt.pokemonDelDia(anio:mes:dia:)`: la función suelta
* de un fichero llega a Swift dentro de una clase con el nombre del fichero + `Kt`
* (la «fachada» de «Los tipos que cruzan a Swift»).
*/
fun pokemonDelDia(anio: Int, mes: Int, dia: Int): Int =
PokemonOfTheDay.idParaFecha(LocalDate(anio, mes, dia)) // construye la fecha aquí, en Kotlin¿Por qué no llamar a PokemonOfTheDay.shared.idParaFecha(fecha:) directamente? Compila, pero obligaría a Swift a construir un tipo de fecha con un nombre que inventa el compilador para una librería ajena (Kotlinx_datetimeLocalDate). Con el puente, Swift solo maneja Int32.
Paso 2 · Siete notificaciones por adelantado
El archivo. RecordatorioDiario.swift reúne todo lo que el recordatorio hace con el sistema: leer el permiso, pedirlo y programar los avisos. Es un enum sin casos, que en Swift es la forma de agrupar funciones sueltas sin poder crear instancias (como la caja de constantes de Ajustes que sobreviven: UserDefaults y @AppStorage).
Créalo. Con el método de La lista en iOS: en la Terminal, en la carpeta de tu proyecto (la que contiene shared e iosApp), mkdir -p iosApp/iosApp/Daily (crea la carpeta Daily si no existe); en Xcode, clic derecho sobre la carpeta Daily → New File from Template… → Swift File → nombre RecordatorioDiario → Create. Plan B, también desde esa carpeta: mkdir -p iosApp/iosApp/Daily y touch iosApp/iosApp/Daily/RecordatorioDiario.swift.
Este fichero se escribe en seis trozos, uno debajo del otro, dentro del mismo enum. Pega cada trozo justo debajo del anterior, sin cerrar el enum hasta el último. Mientras no llegues al final, Xcode marcará errores en rojo (una llave sin cerrar, una función sin terminar): es normal, desaparecen al pegar el último trozo.
Primero la cabecera, con el número de días y el prefijo de los identificadores:
import Foundation
import UserNotifications // el centro de notificaciones de iOS
import Shared // el framework de Kotlin
/// El recordatorio diario en iOS.
///
/// La diferencia de fondo con Android: aquí no hay trabajo en segundo plano que
/// se ejecute cada día. Se programan los avisos por adelantado y el sistema los
/// dispara, con el contenido ya escrito.
enum RecordatorioDiario {
/// Cuántos días se programan por adelantado. iOS guarda como mucho 64
/// notificaciones pendientes por app; siete queda lejos.
static let diasProgramados = 7
/// Los identificadores son fijos: pokemon-del-dia-0 … pokemon-del-dia-6.
static let identificadorPrefijo = "pokemon-del-dia-"Segundo trozo, justo debajo de la constante anterior: las dos funciones del permiso. Traducen lo que dice iOS a los tres estados del núcleo (EstadoDelPermiso llega a Swift con sus nombres en camel case: SIN_PREGUNTAR es .sinPreguntar):
/// ¿En qué estado está el permiso? (el de iOS tiene más casos que el nuestro)
static func estadoDelPermiso() async -> EstadoDelPermiso {
let ajustes = await UNUserNotificationCenter.current().notificationSettings()
switch ajustes.authorizationStatus {
case .notDetermined: return .sinPreguntar // nunca se ha preguntado
// .provisional: entrega sin molestar, sin haber enseñado el diálogo.
// .ephemeral: permiso temporal que iOS da en situaciones especiales. Aquí también es «sí».
case .authorized, .provisional, .ephemeral: return .concedido
default: return .denegado // .denied: iOS no vuelve a preguntar
}
}
/// Enseña el diálogo del sistema (solo la primera vez) y devuelve el resultado.
static func pedirPermiso() async -> EstadoDelPermiso {
do {
let concedido = try await UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge])
return concedido ? .concedido : .denegado
} catch {
return .denegado
}
}Tercer trozo: antes de programar, una función que prepara el contenido de un día. Calcula el Pokémon con el puente del paso 1 y le pone dentro el enlace que abre su ficha, en el userInfo del ejemplo de arriba. Por ahora nadie lee ese enlace: lo leerá la lección siguiente. Va dentro del enum, debajo de las dos funciones del permiso, y es una función aparte porque la prueba del paso 5 la reutilizará:
/// El contenido de la notificación de un día: el Pokémon y el enlace a su ficha.
private static func contenido(anio: Int, mes: Int, dia: Int) -> UNMutableNotificationContent {
// El puente de tres enteros (Swift ve los Int de Kotlin como Int32).
let id = DailyIosKt.pokemonDelDia(anio: Int32(anio), mes: Int32(mes), dia: Int32(dia))
let contenido = UNMutableNotificationContent()
contenido.title = "El Pokémon de hoy"
contenido.body = "Descubre quién es el número \(id)"
contenido.sound = .default
// El enlace viaja en el userInfo (un diccionario libre): al tocar la
// notificación, la app lo lee y abre la misma ficha que abriría desde fuera.
contenido.userInfo = ["enlace": DestinationKt.toDeepLink(DestinationDetalle(pokemonId: Int32(id)))]
return contenido
}Esa línea del userInfo se lee de dentro hacia fuera. DestinationDetalle(pokemonId: …) es el destino «la ficha de este Pokémon» (el Destination.Detalle de Kotlin, que escribiste en Deep links en iOS). toDeepLink es la función de Kotlin que convierte un destino en su enlace de texto (pokedex://pokemon/…). Como está suelta en el fichero Destination.kt, Swift la ve dentro de DestinationKt, igual que DailyIosKt del paso 1. Y Int32(id): id ya llega del puente como Int32, así que ese Int32(...) no cambia nada; solo deja a la vista el tipo entero que pide Kotlin.
Y ahora la función que programa, en dos trozos (el cuarto y el quinto). Va dentro del enum, debajo de contenido.
Primero, borrar antes de reprogramar y recorrer los días. Este trozo termina a mitad del for: Xcode se quejará de llaves sin cerrar hasta que pegues el siguiente.
/// Programa los próximos días, a la hora local indicada.
static func programar(hora: Int = 9) async {
let centro = UNUserNotificationCenter.current()
// Se borra lo anterior antes de reprogramar: si no, los días ya
// programados se acumulan con los nuevos y se llega al límite.
centro.removePendingNotificationRequests(
withIdentifiers: (0..<diasProgramados).map { "\(identificadorPrefijo)\($0)" }
)
let calendario = Calendar.current
let hoy = calendario.startOfDay(for: Date()) // hoy a las 00:00
for dia in 0..<diasProgramados {
// La fecha de «hoy + dia días».
guard let fecha = calendario.date(byAdding: .day, value: dia, to: hoy) else { continue }
// Año, mes, día y la hora pedida: cuándo debe sonar.
var cuando = calendario.dateComponents([.year, .month, .day], from: fecha)
cuando.hour = hora
cuando.minute = 0
// `Date()` es «ahora». Si ese momento ya pasó (hoy, después de las 9),
// se salta este día. La coma del `guard` es «y además»: las dos condiciones
// tienen que cumplirse, y `momento > Date()` compara dos fechas (mayor = más tarde).
guard let momento = calendario.date(from: cuando), momento > Date() else { continue }
// Los tres números de la fecha, para pedir el contenido de ese día.
guard let anio = cuando.year, let mes = cuando.month, let diaDelMes = cuando.day else { continue }Es el mismo recorrido del ejemplo de las plantas, con una diferencia: aquí no se usa !. Y un detalle del bucle: si la hora de hoy ya pasó, no se programa nada para hoy, porque un aviso con una fecha pasada no sonaría nunca. guard let ... else { continue } desenvuelve el valor y, si es nil, salta al día siguiente del bucle (guard let y textos que no son números).
Los identificadores son fijos y eso hace que reprogramar sea repetible. Añadir una petición con un identificador que ya existe la sustituye: por eso dos llamadas seguidas a programar() no dejan catorce avisos. Pasa de verdad: en el paso 3 el botón de Ajustes programa, y en el paso 4 la app también programa al volver a primer plano, cosa que ocurre justo al cerrarse el diálogo de permiso (mientras está en pantalla, la app pasa a inactiva; al cerrarse, vuelve a activa y se dispara la reposición). Con identificadores aleatorios, cada apertura de la app dejaría siete más, y a la décima apertura estarías en el límite.
Quinto trozo, un disparador por día, sin repetición, y la petición que lo junta todo. Pégalo justo debajo de la línea guard let anio…, todavía dentro del for; este trozo sí cierra el for y la función:
// Un disparador por día con `repeats: false`, no uno repetido: si se
// repitiera, el contenido sería siempre el mismo y el «Pokémon del
// día» enseñaría el de hoy para siempre.
let disparo = UNCalendarNotificationTrigger(dateMatching: cuando, repeats: false)
let peticion = UNNotificationRequest(
identifier: "\(identificadorPrefijo)\(dia)",
content: contenido(anio: anio, mes: mes, dia: diaDelMes),
trigger: disparo
)
try? await centro.add(peticion) // entrega la petición al sistema
}
}Esa es la decisión que más tienta hacer al revés. repeats: true con hora nueve de la mañana es una sola notificación y parece la solución elegante. Pero el contenido de una notificación se escribe al programarla, no al dispararla: el texto se congela. Al día siguiente saldría el mismo número, y el «Pokémon del día» sería el Pokémon del día en que instalaste la app.
Sexto y último trozo, debajo de programar: la función que cuenta cuántas quedan, que la pantalla de Ajustes usará, y que cierra el enum (la } final). filter se queda con los avisos cuyo identificador empieza por el prefijo (hasPrefix) y count los cuenta:
/// Cuántos avisos del Pokémon del día quedan pendientes.
static func pendientes() async -> Int {
await UNUserNotificationCenter.current().pendingNotificationRequests()
.filter { $0.identifier.hasPrefix(identificadorPrefijo) }
.count
}
}Paso 3 · La sección de Ajustes
Abre iosApp/iosApp/Settings/SettingsView.swift. Primero, dos estados y la acción para abrir una URL, junto a los @State de la vista. @Environment(\.openURL) pide a SwiftUI, por su clave \.openURL, una función que abre una URL. Para los Ajustes de iOS, la dirección es UIApplication.openSettingsURLString. UIApplication es la clase de iOS que representa a tu app en marcha, y openSettingsURLString es la dirección de la aplicación Ajustes. Su idea es abrir la página de tu app dentro de Ajustes, pero en el simulador solo abre la pantalla principal de Configuración, sin entrar en tu app (así se vio con iOS 26.2). Si te pasa, es cosa del simulador y no de tu código. Es la misma idea que abrirAjustesDeNotificaciones en Notificaciones en Android.
@State private var permiso: EstadoDelPermiso = .sinPreguntar // lo que sabe el sistema
@State private var pendientes = 0 // avisos programados
@Environment(\.openURL) private var openURL // para abrir los Ajustes de iOSDespués, dentro del Form, entre la sección de favoritos y la de «Acerca de», la sección nueva (Section { … } header: { … } footer: { … } es un grupo de filas con un título arriba, header, y una nota abajo, footer). Dice la verdad en cada estado y, con el permiso denegado, lleva a los Ajustes de iOS, que es lo único que queda (iOS no vuelve a preguntar):
Section {
Button {
Task {
switch permiso {
case .concedido:
await RecordatorioDiario.programar() // reprogramar
case .sinPreguntar:
// Pide el permiso y, si lo dan, programa.
permiso = await RecordatorioDiario.pedirPermiso()
if permiso == .concedido { await RecordatorioDiario.programar() }
default:
// Denegado: iOS no vuelve a preguntar. Se lleva al usuario a los ajustes.
if let url = URL(string: UIApplication.openSettingsURLString) { openURL(url) }
}
pendientes = await RecordatorioDiario.pendientes()
}
} label: {
// `LabeledContent` pone una etiqueta a la izquierda y un valor a la derecha.
LabeledContent("Pokémon del día") {
switch permiso {
case .concedido:
Text(pendientes > 0 ? "\(pendientes) días programados" : "Activar")
case .sinPreguntar:
Text("Activar")
default:
Text("Desactivadas").foregroundStyle(.secondary)
}
}
}
} header: {
Text("Notificaciones")
} footer: {
Text("Se programan siete días por adelantado. iOS admite 64 notificaciones pendientes por app.")
}Por último, en la vista, una .task que lee el estado cuando la pestaña aparece. Ponla encima de la .task que ya tenías, también después de la } que cierra el NavigationStack:
.task {
permiso = await RecordatorioDiario.estadoDelPermiso()
pendientes = await RecordatorioDiario.pendientes()
}Paso 4 · Reponer los avisos al abrir la app
Siete días se acaban. Si nadie reprograma, a los siete días la app deja de avisar. La ventana se repone cada vez que la app pasa a primer plano, y eso es lo que fija el tamaño de siete: de sobra para que un usuario que abre la app una vez por semana nunca se quede sin aviso. Tiene un precio: como el contenido se escribe al programar, si un día cambiara la regla del Pokémon del día, los avisos ya programados seguirían diciendo lo antiguo hasta que se abra la app y se reprogramen. Con siete días, ese desfase es corto; con treinta sería largo, y con 64 se acercaría al tope.
scenePhase es el valor de SwiftUI que dice si la app está activa, en segundo plano o inactiva (lo viste en La app, sus escenas y el paquete Torneo). En iosApp/iosApp/ContentView.swift, primero la propiedad, junto a las otras:
@Environment(\.scenePhase) private var scenePhaseY después el observador, justo debajo de .preferredColorScheme(ajustes.tema.esquema) y antes de tu .onOpenURL. .onChange(of:) ejecuta su bloque cada vez que el valor observado cambia; initial: true hace que el bloque se ejecute también una vez al aparecer la vista, no solo cuando la fase cambia (sin él, el primer arranque no repondría nada):
// `initial: true` también lo ejecuta al arrancar, no solo al volver.
.onChange(of: scenePhase, initial: true) { _, fase in
guard fase == .active else { return }
Task {
// Solo si el usuario ya dijo que sí: reprogramar sin permiso no hace nada.
if await RecordatorioDiario.estadoDelPermiso() == .concedido {
await RecordatorioDiario.programar()
}
}
}El bloque recibe dos valores, el anterior y el nuevo; el _ ignora el anterior. Como programar() borra antes de añadir, abrir la app diez veces deja siempre siete.
Paso 5 · Una prueba sin esperar a las 9:00
Los avisos reales suenan a las 9:00 de los próximos días: no vas a esperar a mañana para ver un banner. Así que añade un disparador de prueba, temporal, que programa el aviso de hoy para dentro de 20 segundos. Usa UNTimeIntervalNotificationTrigger (suena pasados unos segundos, en vez de a una hora del calendario) y reutiliza la función contenido del paso 2, así que lo que pruebas es el mismo contenido. Su identificador no empieza por pokemon-del-dia-, así que pendientes() no lo cuenta.
En RecordatorioDiario.swift, dentro del enum, entre pendientes() y la última } (la que cierra el enum). TimeInterval es el tipo de Swift para una cantidad de segundos (un número con decimales):
/// SOLO PARA PROBAR. Programa el aviso de hoy dentro de `segundos` segundos.
static func programarPrueba(segundos: TimeInterval = 20) async {
let hoy = Calendar.current.dateComponents([.year, .month, .day], from: Date())
guard let anio = hoy.year, let mes = hoy.month, let dia = hoy.day else { return }
let peticion = UNNotificationRequest(
identifier: "prueba-pokemon-del-dia",
content: contenido(anio: anio, mes: mes, dia: dia),
trigger: UNTimeIntervalNotificationTrigger(timeInterval: segundos, repeats: false)
)
try? await UNUserNotificationCenter.current().add(peticion)
}Y en SettingsView.swift, un botón debajo del de «Pokémon del día», dentro de la misma Section { … }, justo antes de la llave que la cierra (} header: {):
// Solo para probar: se borra al final de la lección siguiente.
Button("Probar dentro de 20 segundos") {
Task { await RecordatorioDiario.programarPrueba() }
}Son andamios, no parte de la app: los quitarás en la lección siguiente, cuando termines de probar el toque.
Pruébalo
Compila una vez (Product → Build, Cmd+B) para que Xcode regenere el framework Shared con DailyIosKt; si no, saldrá como «cannot find». Después ejecuta iosApp en un simulador (Cmd+R) y abre la pestaña Ajustes.
- La sección Notificaciones dice «Activar». Tócala: iOS enseña su diálogo. Allow (en un simulador en español, Permitir) → la fila pasa a «7 días programados». Si abres la app después de las 9:00, dirá «6 días programados»: el aviso de hoy ya pasó y no se programa.
- Detén la app (botón Stop de Xcode) y ábrela otra vez con Cmd+R: sigue en 7 (o 6), no 14: se borra antes de reprogramar. (Al reiniciar, la app abre en la pestaña Pokédex: vuelve a Ajustes para ver el número.)
- Con la app en segundo plano. Toca Probar dentro de 20 segundos y, de inmediato, manda la app al fondo con el botón Inicio del simulador (Device → Home, o Cmd+Mayús+H). A los 20 segundos baja un banner «El Pokémon de hoy · Descubre quién es el número…».
- Toca el banner. La app se abre, pero no te lleva a la ficha de ese Pokémon: aparece donde la dejaste (o en la lista, si la habías cerrado). Todavía nadie lee el enlace del
userInfo. De eso trata la lección siguiente.
Si dejas la app delante, en el paso 3, no baja ningún banner: iOS no enseña por su cuenta un aviso cuando la app está abierta, hace falta el delegado con willPresent. También viene en la lección siguiente.
Para dejar los tests verificados, en la Terminal, en la carpeta de tu proyecto, lanza los de las tres plataformas (cada comando cuenta los de una), sin filtro, y abre los informes, como en Ajustes en el núcleo: ./gradlew :shared:jvmTest :shared:testAndroidHostTest :shared:iosSimulatorArm64Test y después open shared/build/reports/tests/jvmTest/index.html (y lo mismo con testAndroidHostTest e iosSimulatorArm64Test). Esta lección no añade ninguno: siguen en 80, 44 y 47.
Punto de control
- Existen
DailyIos.ktenshared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/feature/dailyyRecordatorioDiario.swiften la carpetaDailyde la app. - En Ajustes hay una sección «Notificaciones» con el estado del permiso, y con el permiso denegado la fila lleva a los Ajustes de iOS.
- Al activarla, la fila pasa a «7 días programados» (o 6, según la hora).
- Cerrar y abrir la app no acumula: siguen siendo siete o seis, no catorce.
- Con la app en segundo plano, el aviso de prueba baja como banner; al tocarlo, todavía no abre la ficha.
- Cmd+B compila sin errores, y los informes de tests dan 80 (JVM), 44 (Android host) y 47 (iOS): sin tests nuevos.
Comprueba lo que entendiste
8 preguntas · necesitas 6 para completar la lección
1.En iOS no hay un trabajo diario que decida qué mostrar. ¿Cómo se resuelve?
2.¿Qué pasa si una app programa noventa notificaciones locales?
3.¿Por qué cada día lleva su propio disparador con
repeats: false?4.¿Por qué se reprograman los siete días cada vez que la app pasa a primer plano?
5.¿Qué hace
initial: trueen.onChange(of: scenePhase, initial: true)?6.La app llama a
programar()dos veces seguidas (el botón de Ajustes y la reposición al volver a la app). ¿Cuántos avisos del Pokémon del día quedan?7.El paso 1 crea
DailyIos.kt, una función que recibe tres enteros. ¿Por qué no llamar directamente a la función del núcleo que recibe una fecha?8.La fila «Pokémon del día» de Ajustes aparece como «Desactivadas» porque el usuario dijo que no. ¿Qué hace al tocarla?