L’API Screen Time d’iOS en 2026 : ce que la doc d’Apple ne dit pas
J’ai passé août et septembre 2026 à publier un bloqueur d’applis sur iOS. Il utilise les frameworks de Temps d’écran (FamilyControls, DeviceActivity, ManagedSettings et ManagedSettingsUI) depuis React Native, via Expo SDK 57 et la bibliothèque react-native-device-activity (0.6.1). La documentation d’Apple te dit ce qu’est chaque type. Elle dit très peu de choses sur la façon dont ils se comportent sur un vrai téléphone, pendant la nuit, sous iOS 26.
Voici la liste que j’aurais aimé avoir. Les comportements que j’ai constatés sur un appareil mais que je ne trouve documentés nulle part sont marqués observé ; ils ne sont peut-être pas vrais sur toutes les versions d’iOS. Les points propres à la bibliothèque React Native sont marqués bibliothèque ; saute-les si tu écris directement en Swift.
- Les pièces du puzzle
- Entitlements : un par bundle ID
- L’App Group est le seul canal
- Les jetons sont opaques, et le sélecteur
- Mesurer l’utilisation avec des événements de seuil
- Les plannings ne démarrent pas quand tu crois
- stopMonitoring déclenche intervalDidEnd
- Les seuils fantômes d’iOS 26
- Un shield ne peut pas ouvrir ton appli
- Garder ses extensions personnalisées au prebuild
- Xcode 26, le simulateur et les tests
- Et sur Android
- Check-list
- Réponses rapides
- Pourquoi j’ai eu besoin de tout ça
1. Les pièces du puzzle
« L’API Screen Time », ce sont quatre frameworks et, en pratique, trois extensions d’appli :
- FamilyControls : l’autorisation (
AuthorizationCenter, avec.individualpour une appli d’autocontrôle) et leFamilyActivityPicker, qui renvoie uneFamilyActivitySelectionde jetons d’applis, de catégories et de domaines web. - ManagedSettings : un
ManagedSettingsStoredans lequel tu mets ces jetons pour les bloquer derrière un shield. - DeviceActivity : les plannings et les événements de seuil d’utilisation. Les callbacks (
intervalDidStart,intervalDidEnd,eventDidReachThreshold, et leurs variantes d’avertissement) sont livrés à une extensionDeviceActivityMonitor, pas à ton appli. - ManagedSettingsUI : une extension
ShieldConfigurationDataSourcequi met en forme le shield (l’écran de blocage), et une extensionShieldActionDelegatequi gère ses boutons.
Tout l’intérêt est dans les extensions. Quand un utilisateur n’a plus de temps, mon appli est en général suspendue ou tuée, donc c’est l’extension de monitoring qui remet le shield en place. Conçois tout comme si le processus de ton appli n’était jamais là au moment où les choses se passent.
2. Entitlements : une demande par bundle ID
L’entitlement Family Controls (Distribution) est approuvé par bundle ID. Avec trois extensions, ça fait quatre demandes. Les miennes :
com.honorfit.blocker(l’appli)com.honorfit.blocker.ShieldConfigurationcom.honorfit.blocker.ShieldActioncom.honorfit.blocker.ActivityMonitorExtension
Une fois qu’elles sont approuvées, active la capability Family Controls (Distribution) sur chaque App ID dans le portail développeur, puis crée un App Group et rattache-le aux quatre. S’il en manque un, la provision de cette cible échoue. Les approbations ne sont pas instantanées, alors envoie les quatre demandes dès le premier jour, avant d’avoir écrit beaucoup de code.
Au final, le fichier d’entitlements de chaque extension est identique :
<key>com.apple.developer.family-controls</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.honorfit.blocker</string>
</array>
Sur Expo/EAS, le service de build ne crée de profils de provisioning que pour les extensions qu’il connaît. Je déclare chaque cible, avec ses entitlements, sous extra.eas.build.experimental.ios.appExtensions dans 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. L’App Group est le seul canal
Ton appli et ses extensions sont des processus séparés qui ne tournent presque jamais en même temps. La seule chose qu’ils partagent, c’est le conteneur de l’App Group, en général via UserDefaults(suiteName:). Tout ce dont les extensions ont besoin (la sélection enregistrée, le texte du shield, quoi faire quand un seuil se déclenche) doit y être écrit à l’avance.
react-native-device-activity pousse cette logique jusqu’au bout. L’appli écrit une liste d’« actions » sous une clé du genre actions_for_{activity}_{callback}_{event}, et l’extension de monitoring va les chercher au moment où le callback se déclenche. Conséquence pratique : tu peux changer ce que fait un moniteur déjà lancé sans le redémarrer. Les extensions appellent aussi CFPreferencesAppSynchronize avant de lire, pour voir les dernières écritures de l’appli :
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. Les jetons sont opaques, et le sélecteur
Le sélecteur donne à ton appli des ApplicationToken, des ActivityCategoryToken et des WebDomainToken, et rien d’autre. C’est voulu : l’appli ne peut pas retransformer un jeton en « Instagram ». Ce qu’elle peut faire, c’est les compter, donc mon écran de liste de blocage affiche « 3 applis, 1 catégorie ». C’est tout le modèle de confidentialité, et il est bon : mon appli ne sait littéralement pas ce que tu as bloqué.
FamilyActivityPicker d’Apple. L’utilisateur voit les noms et les icônes des applis ; ton appli ne reçoit en retour que des jetons opaques.L’extension de shield, c’est différent. Là, le système te passe une Application pour l’appli bloquée, et j’utilise son localizedDisplayName dans le texte du shield. L’appli elle-même ne voit jamais que des jetons.
Deux remarques pratiques :
- Les catégories restent des catégories. Si l’utilisateur choisit « Réseaux sociaux », tu reçois un jeton de catégorie, pas les applis qu’elle contient, sauf si la sélection a été créée avec
includeEntireCategory. Je le laisse àfalsepour que les nombres affichés soient honnêtes (« 1 catégorie », pas un nombre d’applis que je ne peux pas nommer). - bibliothèque Le sélecteur persistant de react-native-device-activity ne précharge la sélection enregistrée (les coches à la réouverture) que si tu lui passes la prop
includeEntireCategory, avec n’importe quelle valeur. C’est son setter natif qui déclenche le chargement. Sans la prop, le sélecteur s’ouvre vide à chaque fois, même si la sélection a bien été enregistrée.
<DeviceActivitySelectionSheetViewPersisted
familyActivitySelectionId="distracting-apps"
includeEntireCategory={false} // must be present, or nothing preloads
onSelectionChange={(e) => setCounts(e.nativeEvent)}
onDismissRequest={() => setPickerVisible(false)}
/>
bibliothèque Encore une : blockSelection de la bibliothèque ajoute des jetons à une liste de blocage persistante. Quand l’utilisateur modifie sa sélection, les applis retirées restent bloquées. J’appelle d’abord resetBlocks(), puis je rebloque la sélection actuelle, à chaque fois.
5. Mesurer l’utilisation avec des événements de seuil
HonorFit te donne un solde de minutes qui ne diminue que lorsqu’une appli bloquée est à l’écran. DeviceActivity n’a pas d’appel du genre « dis-moi combien de temps il l’a utilisée » ; il a des événements de seuil qui se déclenchent une fois que l’utilisation cumulée d’une sélection atteint une durée. Donc j’enregistre un événement toutes les 15 secondes d’utilisation, nommé en minutes : used-0.25, used-0.5, … jusqu’au solde. Les quarts sont exacts en virgule flottante binaire, donc les noms font l’aller-retour par Number() sans perte. Sous la bibliothèque, c’est l’API d’Apple toute simple :
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 (la valeur par défaut) signifie que l’utilisation antérieure à l’enregistrement des événements ne compte pas. C’est important pour l’astuce de planning de la section suivante.
Quand le dernier événement se déclenche, l’extension de monitoring remet le shield et envoie une notification « temps écoulé ». Chaque événement de 15 secondes met à jour une seule notification avec un identifiant fixe, donc elle se remplace sur place et sert de décompte en direct approximatif. interruptionLevel = .passive la livre discrètement dans le Centre de notifications, sans bannière.
bibliothèque Ne mets pas de tirets bas (underscores) dans les noms d’événements ou d’activités. La bibliothèque enregistre les événements déclenchés sous events_{activity}_{callback}_{eventName} et les relit en découpant sur _. Un événement appelé used_7 revient sous le nom used, et plus rien ne correspond. Le symptôme : un solde qui ne baissait jamais.
6. Les plannings ne démarrent pas quand tu crois
Si tes événements de seuil ne se déclenchent jamais, sans un bruit, c’est probablement ça.
« Maintenant » est déjà dans le passé
observé Un DeviceActivitySchedule non répété est comparé à des composants de date, et le moniteur exécute le prochain intervalle qui correspond. Si tu passes l’heure actuelle comme intervalStart, le temps que le framework l’évalue, ce moment est passé. La prochaine correspondance, c’est demain, et aujourd’hui rien ne se déclenche : pas de seuils, pas de callbacks, pas d’erreur. La solution est d’antidater le début pour que « maintenant » soit dans l’intervalle :
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);
Antidater ne peut pas faire payer à l’utilisateur le scroll d’avant, grâce à includesPastActivity: false. Ça garde aussi l’intervalle au-dessus du minimum d’Apple : les plannings de moins de 15 minutes sont refusés, ce qui piège tous ceux qui lancent une session juste avant minuit.
Un planning qui contient « maintenant » démarre immédiatement
observé Le revers de la médaille : un planning 00:00–23:59:59 contient l’heure actuelle, donc il démarre maintenant, pas au prochain minuit. J’ai trouvé deux façons de réserver le lendemain :
- Un planning déjà expiré.
[00:00, T), où T est une minute avant l’heure actuelle. L’occurrence du jour est terminée, donc le framework réserve celle de demain. Je m’en sers pour reporter un solde non dépensé au-delà de minuit. repeats: trueavec des actions protégées. L’occurrence du jour démarre immédiatement et ne fait rien (chaque action est protégée, voir la section suivante). Celle de demain fait le vrai travail.
observé N’utilise pas de composants année/mois/jour complets dans un planning pour viser une date précise. Les callbacks des extensions sont devenus peu fiables quand j’ai essayé, ce qui rejoint des signalements sur les Apple Developer Forums (fil 729841). Tiens-t’en à heure/minute/seconde.
Une réserve : ces comportements autour de minuit n’apparaissent que sur un appareil, pendant la nuit. J’ai mis des garde-fous autour, mais je n’ai pas instrumenté autant de vrais minuits que je l’aurais voulu, alors prends cette section comme « ce que j’ai vu », pas comme une spécification.
7. stopMonitoring déclenche intervalDidEnd
observé Appeler stopMonitoring, ou appeler startMonitoring avec le nom d’un moniteur déjà lancé, fait appeler par iOS intervalDidEnd sur ton extension, comme si le planning s’était terminé normalement. Si ton intervalDidEnd remet le shield par sécurité (le mien le fait, pour qu’un moniteur ne puisse pas expirer en laissant des applis débloquées), alors chaque fois que l’appli arrête un moniteur, l’extension bloque tout pendant un bref instant.
C’est un utilisateur qui a signalé le symptôme : Spotify se mettait en pause chaque fois qu’il ouvrait HonorFit. Arrêter le moniteur faisait bloquer la sélection par l’extension un instant, ce qui suspendait Spotify, avant que l’appli ne retire de nouveau les shields.
Deux correctifs, et il faut les deux. D’abord, supprime les actions de l’extension avant d’arrêter, pour qu’elle n’ait rien à exécuter :
function stopActivities(names: string[]): void {
for (const name of names) RNDA.cleanUpAfterActivity(name); // clear actions first
RNDA.stopMonitoring(names); // then stop
}
Ensuite, donne à chaque action d’intervalDidEnd et d’intervalDidStart un horodatage « pas avant » fixé à la vraie limite du planning, moins une minute de marge. L’extension le vérifie avant d’exécuter quoi que ce soit :
// 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 pur : enregistre la vraie heure de fin dans l’App Group et fais en sorte qu’intervalDidEnd abandonne s’il est appelé trop tôt.
8. Les seuils fantômes d’iOS 26
Sur iOS 26.x, eventDidReachThreshold se déclenche parfois sans aucune utilisation réelle : juste après la programmation, ou quand un téléphone resté verrouillé un moment est repris en main ou branché. Apple l’a reconnu dans des fils du forum développeurs (809410, 811305, 814559). Ce serait corrigé dans une bêta de 26.5, mais des gens ont continué à le signaler ensuite. Dans mon appli, le symptôme était « plus de temps » après un passage du téléphone dans la poche, parce que l’extension remettait le shield et que le solde tombait à zéro.
Tu ne peux pas empêcher l’événement de se déclencher, mais tu peux refuser d’y croire. Deux faits sur les vrais seuils font l’essentiel du travail :
- L’utilisation ne peut pas aller plus vite que l’horloge.
used-7ne peut pas légitimement se déclencher moins de 7 minutes après l’armement du moniteur. Chaque action de seuil porteneverTriggerBefore = armedAt + N minutes − 5s, donc l’extension ignore les déclenchements impossiblement précoces. - L’utilisation réelle arrive petit à petit ; les fantômes arrivent en rafale. Quand l’appli relit les événements déclenchés, elle considère qu’il s’agit d’un fantôme si le seuil le plus bas et le plus haut se sont déclenchés à environ 3 secondes d’écart mais couvrent une minute d’utilisation ou plus.
// 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
}
Le garde-fou 2 tourne dans l’appli, pas dans l’extension, donc un fantôme peut quand même remettre le shield. Le solde, lui, survit, et la prochaine fois que l’appli est ouverte, elle retire le shield. Détecter les rafales dans l’extension demanderait du Swift sur mesure, que je n’ai pas encore écrit.
observé Un problème voisin va dans l’autre sens : des seuils qui devraient se déclencher et ne le font pas. Avec une utilisation par à-coups sur iOS 26, j’ai vu le premier seuil de 15 secondes d’un nouveau moniteur se déclencher à l’heure, puis les suivants être fusionnés ou carrément perdus (used-0.25 s’est déclenché, used-0.5 et au-delà jamais). Mon contournement : considérer comme périmé tout moniteur qui a déjà mesuré de l’utilisation, et en réarmer un nouveau, sur le solde restant, chaque fois que l’appli passe en arrière-plan. Ça fait cadeau de 15 secondes d’utilisation au maximum par redémarrage, ce qui me va très bien.
9. Un shield ne peut pas ouvrir ton appli
Le design évident, c’est un bouton sur le shield qui dit « Gagner plus de temps » et ouvre ton appli. Il n’y a aucun moyen officiel de le faire ; le DTS d’Apple dit qu’il n’existe pas d’API permettant à une extension de shield de lancer son appli parente. Un ShieldActionDelegate répond à un appui sur un bouton avec une ShieldActionResponse (.none, .close ou .defer), et aucune d’elles n’ouvre quoi que ce soit.
observé Le contournement qu’on trouve partout (NSExtensionContext().open(url) depuis l’extension ShieldAction, que la bibliothèque expose sous le nom openUrl) n’a rien fait sur les iOS actuels quand je l’ai testé sur un appareil en septembre 2026. Le bouton fermait simplement le shield. Envoyer une notification depuis l’appui sur le bouton n’était pas fiable non plus chez moi.
Ce que je publie à la place : le shield a un seul bouton « Fermer », et l’extension ShieldConfiguration envoie une notification locale chaque fois que le shield est affiché. L’utilisateur appuie sur la bannière et arrive dans l’appli. L’appli écrit à l’avance le contenu de la notification dans l’App Group, et retire la notification quand elle revient au premier plan. iOS peut demander la configuration du shield plusieurs fois par affichage, donc l’envoi est soumis à un délai minimum (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
}
C’est du bricolage, et ça dépend de l’autorisation des notifications par l’utilisateur, mais c’est la seule route vers l’appli qui a marché de façon fiable chez moi.
10. Garder ses extensions personnalisées au prebuild
bibliothèque Si tu utilises Expo, expo prebuild exécute le config plugin de react-native-device-activity, qui par défaut copie les sources d’extension d’origine de la bibliothèque dans ./targets à chaque lancement. Ma ShieldConfiguration personnalisée a été remplacée sans prévenir. La solution tient en une option du plugin :
["react-native-device-activity", {
"appleTeamId": "XXXXXXXXXX",
"appGroup": "group.com.honorfit.blocker",
"copyToTargetFolder": false
}]
Le prix à payer : quand tu mets à jour la bibliothèque, compare à la main son dossier targets/ et son Swift partagé avec les tiens, et réapplique tes modifications.
11. Xcode 26, le simulateur et les tests
- Compile avec le SDK iOS 26. App Store Connect refuse désormais les envois compilés avec un SDK antérieur à iOS 26 (ITMS-90725). Sur EAS, je fixe l’image de build sur
macos-tahoe-26.5-xcode-26.6. - Le simulateur ne t’emmène qu’à mi-chemin. Sur les simulateurs iOS 26, la demande d’autorisation et le sélecteur s’affichent bien. Mais l’autorisation n’aboutit jamais (« Allow with Passcode », « Autoriser avec le code », demande un code d’appareil que le simulateur ne peut pas définir), le sélecteur n’a aucune appli à lister, et l’extension de monitoring ne s’exécute jamais. Tout ce qui est dans les sections 5 à 9 demande un vrai appareil.
- Conditionne les appels ManagedSettings à l’autorisation. Toucher au store sans autorisation faisait apparaître une demande de code système sur le simulateur, ce qui cassait mes tests d’interface. Je vérifie le statut d’autorisation avant tout appel lié au shield.
- Affiche les erreurs d’autorisation dans l’interface. Les testeurs TestFlight n’ont pas de console. Si
requestAuthorizationéchoue en silence, le bouton a juste l’air cassé. J’affiche l’erreur et le statut dans une alerte. - Teste pendant la nuit. Mes pires bugs (bloqué toute la nuit alors qu’il restait du temps, musique qui se met en pause) étaient invisibles en revue de code et ne se reproduisaient que sur un appareil, au bout de plusieurs heures.
12. Et sur Android
Pour comparer : Android n’a pas d’équivalent officiel, donc tu le construis toi-même. Le bloqueur Android d’HonorFit est un service de premier plan (type specialUse, puisque le blocage d’applis ne rentre dans aucun des types listés) qui interroge UsageStatsManager une fois par seconde tant que l’écran est allumé, pour savoir quelle appli est au premier plan. Quand une appli bloquée est affichée et que le solde est à zéro, il dessine par-dessus un écran de verrouillage TYPE_APPLICATION_OVERLAY (« Afficher par-dessus d’autres applis »). Un receiver de démarrage et une alarme de surveillance relancent le service après un redémarrage, une mise à jour de l’appli ou le passage d’un gestionnaire de batterie agressif de constructeur.
Les compromis vont dans le sens inverse d’iOS. Tu vois les vrais noms de paquets, donc la confidentialité est une promesse que tu tiens toi-même plutôt qu’une garantie imposée par l’OS. Une appli bloquée peut rester visible jusqu’à un intervalle d’interrogation, et elle continue de tourner sous la superposition. Et tu vas devoir rédiger des déclarations dans la Play Console pour le type de service de premier plan, l’autorisation de superposition et la visibilité des paquets. J’ai volontairement évité un AccessibilityService, qui subit l’examen le plus strict de tous.
13. Check-list
| Symptôme | Cause probable | Solution |
|---|---|---|
| La provision échoue pour une cible | Entitlement ou App Group manquant sur ce bundle ID | Demander Family Controls (Distribution) et rattacher l’App Group pour chaque bundle ID |
| Aucun événement de seuil, jamais | intervalStart = maintenant, donc le moniteur est réservé pour demain | Antidater le début ; garder l’intervalle ≥ 15 min |
| Un moniteur prévu pour demain tourne aujourd’hui | Le planning contient l’heure actuelle | Planning déjà expiré [00:00, T), ou repeats: true avec des actions protégées |
| Les applis se bloquent une fraction de seconde quand ton appli s’ouvre | stopMonitoring déclenche intervalDidEnd | Effacer les actions avant d’arrêter ; protéger avec une heure « pas avant » |
| « Plus de temps » après un téléphone resté inactif | Seuils fantômes d’iOS 26 | Garde-fou sur l’horloge plus filtre anti-rafale |
| Les seuils suivants ne se déclenchent jamais | Perdus/fusionnés en cas d’utilisation intermittente | Réarmer un nouveau moniteur à chaque passage en arrière-plan |
| Le bouton du shield n’ouvre pas l’appli | Aucune API officielle | Envoyer une notification depuis ShieldConfiguration à l’affichage |
| Les événements déclenchés ne correspondent pas (RN) | Tiret bas dans le nom de l’événement | Utiliser used-7, pas used_7 |
| Le sélecteur s’ouvre vide (RN) | Prop includeEntireCategory manquante | La passer, avec n’importe quelle valeur |
14. Réponses rapides
Faut-il l’entitlement Family Controls pour chaque extension ?
Oui. Il est accordé par bundle ID. L’appli plus une extension DeviceActivityMonitor, une ShieldConfiguration et une ShieldAction, ça fait quatre approbations, et chaque App ID a aussi besoin de l’App Group partagé.
Pourquoi eventDidReachThreshold de DeviceActivity ne se déclenche pas ?
Chez moi, c’était en général un planning non répété qui commençait à « maintenant », et qui se retrouvait réservé pour demain. Antidate intervalStart. Sur iOS 26, avec une utilisation par à-coups, des seuils ultérieurs de moins d’une minute disparaissaient aussi ; réarmer un nouveau moniteur a aidé.
Une extension ShieldAction ou ShieldConfiguration peut-elle ouvrir l’appli parente ?
Pas avec une API officielle, et le contournement via NSExtensionContext n’a rien fait sur les iOS actuels lors de mes tests. Ce que je publie, c’est une notification locale envoyée par ShieldConfiguration quand le shield s’affiche.
Peut-on tester l’API Screen Time dans le simulateur iOS ?
En partie. La demande et le sélecteur s’affichent sur les simulateurs iOS 26, mais l’autorisation n’a pas abouti, le sélecteur n’avait rien à lister et l’extension de monitoring ne s’est jamais exécutée. Utilise un appareil.
15. Pourquoi j’ai eu besoin de tout ça
J’ai créé HonorFit, un bloqueur d’applis où tu gagnes ton temps d’écran en faisant des pompes. Il fonctionne sur le système de l’honneur : tu notes tes répétitions, et il n’y a ni caméra, ni IA, ni appel réseau. Tout ce qui précède, c’est ce qu’il a fallu pour que « tes minutes gagnées ne diminuent que lorsqu’une appli bloquée est à l’écran » fonctionne sur un iPhone. Si tu veux le voir tourner, il est sur l’App Store et Google Play. Si tu es ici parce que tu cherches une appli de ce genre plutôt que d’en construire une, j’ai expliqué comment fonctionnent les bloqueurs d’applis avec des pompes et fait un comparatif honnête de ceux qui existent, concurrents compris. Si tu as rencontré un comportement de Temps d’écran qui n’est pas ici, ou constaté que quelque chose ci-dessus est faux sur un iOS plus récent, ça m’intéresse.