La Screen Time API de iOS en 2026: lo que la documentación de Apple no te cuenta
Pasé agosto y septiembre de 2026 sacando un bloqueador de apps en iOS. Usa los frameworks de Tiempo de uso (FamilyControls, DeviceActivity, ManagedSettings y ManagedSettingsUI) desde React Native, con Expo SDK 57 y la biblioteca react-native-device-activity (0.6.1). La documentación de Apple te dice qué es cada tipo. Dice muy poco sobre cómo se comportan en un teléfono real, durante la noche, en iOS 26.
Esta es la lista que me hubiera gustado tener. Lo que vi en un dispositivo pero no encuentro documentado lleva la etiqueta observado; puede que no se cumpla en todas las versiones de iOS. Lo que es específico de la biblioteca de React Native lleva la etiqueta biblioteca; sáltatelo si escribes Swift directamente.
- Las piezas
- Entitlements: uno por bundle ID
- El App Group es el único canal
- Los tokens son opacos, y el selector
- Medir el uso con eventos de umbral
- Los horarios no empiezan cuando crees
- stopMonitoring dispara intervalDidEnd
- Umbrales fantasma en iOS 26
- Un shield no puede abrir tu app
- Conservar extensiones personalizadas al hacer prebuild
- Xcode 26, el Simulador y las pruebas
- Y en Android
- Lista de verificación
- Respuestas rápidas
- Por qué necesité todo esto
1. Las piezas
«La Screen Time API» son cuatro frameworks y, en la práctica, tres extensiones de app:
- FamilyControls: la autorización (
AuthorizationCenter, con.individualpara una app de autocontrol) y elFamilyActivityPicker, que devuelve unaFamilyActivitySelectioncon tokens de apps, categorías y dominios web. - ManagedSettings: un
ManagedSettingsStoreen el que metes esos tokens para ponerles un shield (la pantalla de bloqueo del sistema). - DeviceActivity: horarios y eventos de umbral de uso. Los callbacks (
intervalDidStart,intervalDidEnd,eventDidReachThresholdy sus variantes de aviso) se entregan a una extensiónDeviceActivityMonitor, no a tu app. - ManagedSettingsUI: una extensión
ShieldConfigurationDataSourceque le da estilo al shield y una extensiónShieldActionDelegateque maneja sus botones.
Lo importante son las extensiones. Cuando a un usuario se le acaba el tiempo, mi app suele estar suspendida o cerrada, así que es la extensión del monitor la que vuelve a poner el shield. Diseña como si el proceso de tu app nunca fuera a estar ahí cuando pasan las cosas.
2. Entitlements: una solicitud por bundle ID
El entitlement Family Controls (Distribution) se aprueba por bundle ID. Con tres extensiones, son cuatro solicitudes. Las mías fueron:
com.honorfit.blocker(la app)com.honorfit.blocker.ShieldConfigurationcom.honorfit.blocker.ShieldActioncom.honorfit.blocker.ActivityMonitorExtension
Cuando estén aprobadas, activa la capability Family Controls (Distribution) en cada App ID en el portal de desarrolladores, y luego crea un App Group y asígnalo a los cuatro. Si te falta uno, ese target no se podrá aprovisionar. Las aprobaciones no son instantáneas, así que envía las cuatro solicitudes el primer día, antes de haber escrito mucho código.
El archivo de entitlements de cada extensión termina siendo el mismo:
<key>com.apple.developer.family-controls</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.honorfit.blocker</string>
</array>
En Expo/EAS, el servicio de compilación solo crea perfiles de aprovisionamiento para las extensiones que conoce. Declaro cada target, con sus entitlements, en extra.eas.build.experimental.ios.appExtensions dentro de app.json:
"appExtensions": [
{
"bundleIdentifier": "com.honorfit.blocker.ShieldConfiguration",
"targetName": "ShieldConfiguration",
"entitlements": {
"com.apple.developer.family-controls": true,
"com.apple.security.application-groups": ["group.com.honorfit.blocker"]
}
}
// ...same for ShieldAction and ActivityMonitorExtension
]
3. El App Group es el único canal
Tu app y sus extensiones son procesos separados que casi nunca se ejecutan al mismo tiempo. Lo único que comparten es el contenedor del App Group, normalmente como UserDefaults(suiteName:). Todo lo que necesitan las extensiones (la selección guardada, el texto del shield, qué hacer cuando se dispara un umbral) tiene que estar escrito ahí de antemano.
react-native-device-activity lleva esto hasta el final. La app escribe una lista de «acciones» bajo una clave como actions_for_{activity}_{callback}_{event}, y la extensión del monitor las busca cuando se dispara el callback. Una consecuencia útil: puedes cambiar lo que hace un monitor que ya está corriendo sin reiniciarlo. Las extensiones también llaman a CFPreferencesAppSynchronize antes de leer, así ven lo último que escribió la app:
let appGroup = Bundle.main.object(
forInfoDictionaryKey: "REACT_NATIVE_DEVICE_ACTIVITY_APP_GROUP") as? String
var userDefaults = UserDefaults(suiteName: appGroup)
// in each callback:
CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication)
if let actions = userDefaults?.array(forKey: triggeredBy) { /* run them */ }
4. Los tokens son opacos, y el selector
El selector le entrega a tu app ApplicationTokens, ActivityCategoryTokens y WebDomainTokens, y nada más. Por diseño, la app no puede convertir un token de vuelta en «Instagram». Lo que sí puede hacer es contarlos, así que mi pantalla de bloqueos muestra «3 apps, 1 categoría». Ese es todo el modelo de privacidad, y es bueno: mi app literalmente no sabe qué bloqueaste.
FamilyActivityPicker de Apple. El usuario ve los nombres y los iconos de las apps; tu app solo recibe tokens opacos.La extensión del shield es distinta. Ahí, el sistema te pasa un Application de la app a la que se le pone el shield, y yo uso su localizedDisplayName en el texto del shield. La app en sí solo ve tokens.
Dos notas prácticas:
- Las categorías siguen siendo categorías. Si el usuario elige «Redes sociales», recibes un token de categoría, no las apps que contiene, a menos que la selección se haya creado con
includeEntireCategory. Yo lo dejo enfalsepara que los conteos que muestro sean honestos («1 categoría», no un número de apps que no puedo nombrar). - biblioteca El selector persistente de react-native-device-activity solo precarga la selección guardada (las marcas de verificación al volver a abrirlo) si le pasas la prop
includeEntireCategory, con cualquier valor. Su setter nativo es lo que dispara la carga. Sin la prop, el selector se abre vacío cada vez aunque la selección se haya guardado.
<DeviceActivitySelectionSheetViewPersisted
familyActivitySelectionId="distracting-apps"
includeEntireCategory={false} // must be present, or nothing preloads
onSelectionChange={(e) => setCounts(e.nativeEvent)}
onDismissRequest={() => setPickerVisible(false)}
/>
biblioteca Una más: blockSelection de la biblioteca agrega tokens a una lista de bloqueo persistente. Cuando el usuario edita su selección, las apps que quitó siguen con el shield puesto. Yo llamo primero a resetBlocks() y luego vuelvo a bloquear la selección actual, cada vez.
5. Medir el uso con eventos de umbral
HonorFit te da un saldo de minutos que solo se descuenta mientras una app bloqueada está en pantalla. DeviceActivity no tiene una llamada de «dime cuánto la usaron»; tiene eventos de umbral que se disparan una vez cuando el uso acumulado de una selección llega a una duración. Así que registro un evento por cada 15 segundos de uso, con nombres en minutos: used-0.25, used-0.5, … hasta el saldo. Los cuartos son exactos en punto flotante binario, así que los nombres van y vuelven sin cambios a través de Number(). Debajo de la biblioteca, esto es la API normal de Apple:
let schedule = DeviceActivitySchedule(
intervalStart: DateComponents(hour: 9, minute: 45, second: 0), // see section 6
intervalEnd: DateComponents(hour: 23, minute: 59, second: 59),
repeats: false
)
let event = DeviceActivityEvent(
applications: selection.applicationTokens,
categories: selection.categoryTokens,
webDomains: selection.webDomainTokens,
threshold: DateComponents(minute: 0, second: 15),
includesPastActivity: false // iOS 17.4+
)
try DeviceActivityCenter().startMonitoring(
DeviceActivityName("honorfit-distracting-apps-1790000000000"),
during: schedule,
events: [DeviceActivityEvent.Name("used-0.25"): event] // ...one per step
)
includesPastActivity: false (el valor por defecto) significa que el uso anterior al registro de los eventos no cuenta. Eso importa para el truco del horario de la siguiente sección.
Cuando se dispara el último evento, la extensión del monitor vuelve a aplicar el shield y publica una notificación de «se acabó el tiempo». Cada evento de 15 segundos actualiza una única notificación con un identificador fijo, así que se reemplaza en el mismo lugar y funciona como una cuenta atrás en vivo aproximada. interruptionLevel = .passive la entrega en silencio al Centro de notificaciones, sin banner.
biblioteca No pongas guiones bajos en los nombres de eventos ni de actividades. La biblioteca guarda los eventos disparados bajo events_{activity}_{callback}_{eventName} y los vuelve a leer separando por _. Un evento llamado used_7 vuelve como used, y nunca coincide nada. El síntoma era un saldo que nunca bajaba.
6. Los horarios no empiezan cuando crees
Si tus eventos de umbral nunca se disparan y no hay ningún error, probablemente sea esto.
«Ahora» ya es pasado
observado Un DeviceActivitySchedule sin repetición se compara con componentes de fecha, y el monitor ejecuta el siguiente intervalo que coincida. Si pasas la hora actual del reloj como intervalStart, para cuando el framework lo evalúa ese momento ya pasó. La siguiente coincidencia es mañana, y hoy no se dispara nada: ni umbrales, ni callbacks, ni errores. La solución es adelantar el inicio hacia atrás para que «ahora» quede dentro del intervalo:
const now = new Date();
const backdated = new Date(now.getTime() - 15 * 60_000);
const intervalStart =
backdated.getDate() === now.getDate()
? { hour: backdated.getHours(), minute: backdated.getMinutes(), second: backdated.getSeconds() }
: { hour: 0, minute: 0, second: 0 }; // don't wrap past midnight
await startMonitoring(activityName,
{ intervalStart, intervalEnd: { hour: 23, minute: 59, second: 59 }, repeats: false },
events);
Mover el inicio hacia atrás no puede cobrarle al usuario el scroll anterior, gracias a includesPastActivity: false. Además mantiene el intervalo por encima del mínimo de Apple: los horarios de menos de 15 minutos se rechazan, y eso le pega a cualquiera que empiece una sesión justo antes de la medianoche.
Un horario que contiene «ahora» empieza de inmediato
observado La otra cara: un horario de 00:00–23:59:59 contiene la hora actual, así que empieza ahora, no en la próxima medianoche. Encontré dos formas de reservar el día siguiente:
- Un horario que ya venció.
[00:00, T), donde T es un minuto antes de la hora actual. La instancia de hoy ya terminó, así que el framework reserva la de mañana. Uso esto para conservar un saldo sin gastar después de la medianoche. repeats: truecon acciones protegidas. La instancia de hoy empieza de inmediato y no hace nada (cada acción está protegida, ver la siguiente sección). La de mañana hace el trabajo de verdad.
observado No uses componentes completos de año/mes/día en un horario para apuntar a una fecha concreta. Los callbacks de la extensión se volvieron poco fiables cuando lo intenté, lo que coincide con reportes en los Apple Developer Forums (hilo 729841). Quédate con hora/minuto/segundo.
Una advertencia: estos comportamientos de medianoche solo aparecen en un dispositivo, durante la noche. Construí protecciones alrededor de ellos, pero no he instrumentado tantas medianoches reales como me gustaría, así que toma esta sección como «lo que vi», no como una especificación.
7. stopMonitoring dispara intervalDidEnd
observado Llamar a stopMonitoring, o llamar a startMonitoring con el nombre de un monitor que ya está corriendo, hace que iOS llame a intervalDidEnd en tu extensión como si el horario hubiera terminado de forma natural. Si tu intervalDidEnd vuelve a poner el shield como red de seguridad (el mío lo hace, para que un monitor no pueda vencer y dejar las apps desbloqueadas), entonces cada vez que la app detiene un monitor, la extensión le pone el shield a todo por un instante.
Un usuario reportó el síntoma: Spotify se pausaba cada vez que abría HonorFit. Detener el monitor hacía que la extensión le pusiera el shield a la selección por un momento, lo que suspendía Spotify, antes de que la app volviera a quitar los shields.
Dos arreglos, y hacen falta los dos. Primero, borra las acciones de la extensión antes de detenerla, para que no tenga nada que ejecutar:
function stopActivities(names: string[]): void {
for (const name of names) RNDA.cleanUpAfterActivity(name); // clear actions first
RNDA.stopMonitoring(names); // then stop
}
Segundo, dale a cada acción de intervalDidEnd e intervalDidStart una marca de tiempo de «no antes de» en el límite real del horario, menos un minuto de margen. La extensión la revisa antes de ejecutar nada:
// TypeScript: register the safety net
RNDA.configureActions({
activityName,
callbackName: 'intervalDidEnd',
actions: [{
type: 'blockSelection',
familyActivitySelectionId: 'distracting-apps',
neverTriggerBefore: new Date(intervalEndMs - 60_000),
}],
});
// Swift, in the DeviceActivityMonitor extension
if let neverTriggerBefore = neverTriggerBefore {
let now = Date().timeIntervalSince1970 * 1000
if now < neverTriggerBefore { return false } // skip this action
}
En Swift puro: guarda la hora de fin real en el App Group y haz que intervalDidEnd no haga nada si lo llaman antes de tiempo.
8. Umbrales fantasma en iOS 26
En iOS 26.x, eventDidReachThreshold a veces se dispara sin ningún uso real: justo después de programarlo, o cuando alguien agarra o conecta un teléfono que estuvo un rato bloqueado. Apple lo ha reconocido en hilos de los foros para desarrolladores (809410, 811305, 814559). Según se informó, se corrigió en una beta de 26.5, pero la gente lo siguió reportando después. En mi app, el síntoma era «sin tiempo» después de tener el teléfono en el bolsillo, porque la extensión volvía a poner el shield y el saldo bajaba a cero.
No puedes impedir que el evento se dispare, pero sí puedes negarte a creerle. Dos hechos sobre los umbrales genuinos hacen casi todo el trabajo:
- El uso no puede ir más rápido que el reloj.
used-7no puede dispararse legítimamente menos de 7 minutos después de que se armó el monitor. Cada acción de umbral llevaneverTriggerBefore = armedAt + N minutes − 5s, así que la extensión ignora los disparos imposiblemente tempranos. - El uso real llega de a poco; los fantasmas llegan de golpe. Cuando la app lee los eventos disparados, los trata como fantasma si el umbral más bajo y el más alto se dispararon con unos 3 segundos de diferencia o menos, pero abarcan un minuto de uso o más.
// Guard 1: drop thresholds that fired before they could have been reached
if (at < startedAt + minutes * 60_000 - 5_000) continue;
// Guard 2: lowest and highest fired in the same instant, a minute or more apart
if (highest.minutes - lowest.minutes >= 1 &&
Math.abs(highest.at - lowest.at) <= 3_000) {
return { usedMinutes: 0, partialMs: 0 }; // phantom: ignore the lot
}
La protección 2 corre en la app, no en la extensión, así que un fantasma todavía puede poner el shield. Pero el saldo sobrevive, y la próxima vez que se abre la app, quita el shield. Detectar las ráfagas dentro de la extensión requeriría Swift personalizado, que todavía no he escrito.
observado Hay un problema relacionado que va en sentido contrario: umbrales que deberían dispararse y no lo hacen. Con un uso de entrar y salir en iOS 26, vi que el primer umbral de 15 segundos de un monitor nuevo se disparaba a tiempo, y que los siguientes se agrupaban o se perdían por completo (used-0.25 se disparó, used-0.5 en adelante nunca). Mi solución es tratar cualquier monitor que ya midió uso como obsoleto y armar uno nuevo, sobre el saldo restante, cada vez que la app pasa a segundo plano. Eso perdona hasta 15 segundos de uso por reinicio, y me parece bien.
9. Un shield no puede abrir tu app
El diseño obvio es un botón en el shield que diga «Gana más tiempo» y abra tu app. No hay una forma compatible de hacerlo; Apple DTS dice que no existe ninguna API para que una extensión de shield abra su app principal. Un ShieldActionDelegate responde a un toque de botón con un ShieldActionResponse (.none, .close o .defer), y ninguno de esos abre nada.
observado El truco que vas a encontrar (NSExtensionContext().open(url) desde la extensión ShieldAction, que la biblioteca expone como openUrl) no hizo nada en el iOS actual cuando lo probé en un dispositivo en septiembre de 2026. El botón simplemente cerraba el shield. Publicar una notificación al tocar el botón tampoco me resultó fiable.
Lo que uso en su lugar: el shield tiene un solo botón de «Cerrar», y la extensión ShieldConfiguration publica una notificación local cada vez que el shield se muestra. El usuario toca el banner y llega a la app. La app escribe de antemano el contenido de la notificación en el App Group, y la descarta cuando pasa a primer plano. iOS puede pedir la configuración del shield varias veces por cada vez que se muestra, así que la publicación tiene debounce:
func postShieldShownNotification(placeholders: [String: String?]) {
guard let config = userDefaults?.dictionary(forKey: "honorfitShieldShownNotification"),
let payload = config["payload"] as? [String: Any] else { return }
let minInterval = config["minIntervalSeconds"] as? Double ?? 30
let now = Date().timeIntervalSince1970
let lastSentAt = userDefaults?.double(forKey: "honorfitShieldShownNotificationLastSentAt") ?? 0
if now - lastSentAt < minInterval { return }
userDefaults?.set(now, forKey: "honorfitShieldShownNotificationLastSentAt")
// builds a UNMutableNotificationContent and adds a UNNotificationRequest
// with a fixed identifier, so repeats replace rather than stack
sendNotification(contents: payload, placeholders: placeholders)
}
class ShieldConfigurationExtension: ShieldConfigurationDataSource {
override func configuration(shielding application: Application) -> ShieldConfiguration {
postShieldShownNotification(placeholders: [
"applicationOrDomainDisplayName": application.localizedDisplayName
])
return buildShield(/* title, subtitle, colors from the App Group */)
}
// ...same hook in the category and web-domain overloads
}
Es un parche, y depende de que el usuario permita las notificaciones, pero es la única vía hacia la app que me funcionó de forma fiable.
10. Conservar extensiones personalizadas al hacer prebuild
biblioteca Si usas Expo, expo prebuild ejecuta el config plugin de react-native-device-activity, que por defecto copia en ./targets las fuentes de las extensiones originales de la biblioteca en cada ejecución. Mi ShieldConfiguration personalizada se reemplazó sin avisar. La solución es una opción del plugin:
["react-native-device-activity", {
"appleTeamId": "XXXXXXXXXX",
"appGroup": "group.com.honorfit.blocker",
"copyToTargetFolder": false
}]
El costo: cuando actualices la biblioteca, compara a mano su targets/ y su Swift compartido con los tuyos, y vuelve a aplicar tus cambios.
11. Xcode 26, el Simulador y las pruebas
- Compila con el SDK de iOS 26. App Store Connect ahora rechaza las subidas compiladas con un SDK anterior a iOS 26 (ITMS-90725). En EAS fijo la imagen de compilación en
macos-tahoe-26.5-xcode-26.6. - El Simulador solo te lleva hasta la mitad. En los simuladores de iOS 26, la solicitud de autorización y el selector sí aparecen. Pero la autorización nunca se completa (“Allow with Passcode”, es decir, permitir con código, pide un código del dispositivo que el Simulador no puede configurar), el selector no tiene apps que listar y la extensión del monitor nunca se ejecuta. Todo lo de las secciones 5 a 9 necesita un dispositivo físico.
- Protege las llamadas a ManagedSettings con la autorización. Tocar el store sin autorización hizo saltar una solicitud de código del sistema en el Simulador, y eso rompió mis pruebas de UI. Reviso el estado de la autorización antes de cualquier llamada al shield.
- Muestra los errores de autorización en la UI. Los testers de TestFlight no tienen consola. Si
requestAuthorizationfalla sin decir nada, el botón simplemente parece roto. Yo muestro el error y el estado en una alerta. - Prueba durante la noche. Mis peores bugs (bloqueado toda la noche con tiempo disponible, la música que se pausaba) eran invisibles en la revisión de código y solo se reproducían en un dispositivo a lo largo de horas.
12. Y en Android
Para contrastar: Android no tiene un equivalente oficial, así que lo construyes tú. El bloqueador de HonorFit en Android es un servicio en primer plano (de tipo specialUse, porque bloquear apps no encaja en ninguno de los tipos de la lista) que consulta UsageStatsManager una vez por segundo mientras la pantalla está encendida para saber qué app está al frente. Cuando hay una app bloqueada en pantalla y el saldo es cero, dibuja encima una pantalla de bloqueo TYPE_APPLICATION_OVERLAY («Mostrar sobre otras apps»). Un receptor de arranque y una alarma de vigilancia reinician el servicio después de reinicios, actualizaciones de la app y gestores de batería agresivos de los fabricantes.
Las concesiones van en sentido contrario a las de iOS. Ves los nombres de paquete reales, así que la privacidad es una promesa que tú cumples, no una que impone el sistema operativo. Una app bloqueada puede quedar visible hasta un intervalo de consulta, y sigue ejecutándose debajo de la capa. Y te va a tocar escribir declaraciones en Play Console para el tipo de servicio en primer plano, el permiso de superposición y la visibilidad de paquetes. Evité a propósito un AccessibilityService, que pasa por la revisión más estricta de todas.
13. Lista de verificación
| Síntoma | Causa probable | Solución |
|---|---|---|
| Falla el aprovisionamiento de un target | Falta el entitlement o el App Group en ese bundle ID | Solicita Family Controls (Distribution) y asigna el App Group para cada bundle ID |
| Nunca llega ningún evento de umbral | intervalStart = ahora, así que el monitor queda reservado para mañana | Mueve el inicio hacia atrás; mantén el intervalo ≥ 15 min |
| Un monitor pensado para mañana corre hoy | El horario contiene la hora actual | Horario vencido hoy [00:00, T), o repeats: true con acciones protegidas |
| Las apps reciben un shield fugaz cuando abres tu app | stopMonitoring dispara intervalDidEnd | Borra las acciones antes de detener; protege con una hora de «no antes de» |
| «Sin tiempo» después de que el teléfono estuvo sin usar | Umbrales fantasma de iOS 26 | Protección por reloj más filtro de ráfagas |
| Los umbrales posteriores nunca se disparan | Se pierden o se agrupan con el uso intermitente | Arma un monitor nuevo cada vez que la app pasa a segundo plano |
| El botón del shield no abre la app | No hay API compatible | Publica una notificación desde ShieldConfiguration al mostrarse |
| Los eventos disparados no coinciden (RN) | Guion bajo en el nombre del evento | Usa used-7, no used_7 |
| El selector se abre vacío (RN) | Falta la prop includeEntireCategory | Pásala, con cualquier valor |
14. Respuestas rápidas
¿Necesito el entitlement de Family Controls para cada extensión?
Sí. Se concede por bundle ID. La app más una extensión DeviceActivityMonitor, una ShieldConfiguration y una ShieldAction son cuatro aprobaciones, y cada App ID necesita además el App Group compartido.
¿Por qué no se dispara eventDidReachThreshold de DeviceActivity?
En mi caso solía ser un horario sin repetición que empezaba «ahora», y que queda reservado para mañana. Mueve intervalStart hacia atrás. En iOS 26, con uso de entrar y salir, también se perdían umbrales posteriores de menos de un minuto; volver a armar un monitor nuevo ayudó.
¿Puede una extensión ShieldAction o ShieldConfiguration abrir la app principal?
No con una API compatible, y en mis pruebas el truco de NSExtensionContext no hizo nada en el iOS actual. Lo que uso es una notificación local publicada desde ShieldConfiguration cuando se muestra el shield.
¿Puedo probar la Screen Time API en el Simulador de iOS?
En parte. La solicitud y el selector aparecen en los simuladores de iOS 26, pero la autorización no se completó, el selector no tenía nada que listar y la extensión del monitor nunca se ejecutó. Usa un dispositivo.
15. Por qué necesité todo esto
Construí HonorFit, un bloqueador de apps en el que te ganas el tiempo de pantalla haciendo flexiones. Funciona con el sistema de honor: escribes tus repeticiones, y no hay cámara, ni IA, ni llamadas de red. Todo lo de arriba es lo que hizo falta para que «tus minutos ganados solo se descuentan mientras una app bloqueada está en pantalla» funcionara en un iPhone. Si quieres verlo funcionando, está en el App Store y en Google Play. Si llegaste aquí porque quieres una app así y no construirla, escribí cómo funcionan los bloqueadores de apps con flexiones y un repaso honesto de los que existen, competidores incluidos. Si te topaste con un comportamiento de Tiempo de uso que no está aquí, o viste que algo de lo de arriba ya no es así en un iOS más nuevo, me gustaría saberlo.