L’API Screen Time di iOS nel 2026: quello che la documentazione di Apple non dice
Ad agosto e settembre 2026 ho pubblicato un’app per bloccare le app su iOS. Usa i framework di Screen Time (FamilyControls, DeviceActivity, ManagedSettings e ManagedSettingsUI) da React Native, tramite Expo SDK 57 e la libreria react-native-device-activity (0.6.1). La documentazione di Apple ti dice cos’è ciascun tipo. Dice ben poco su come si comportano su un telefono vero, di notte, su iOS 26.
Questa è la lista che avrei voluto avere. I comportamenti che ho visto su un dispositivo ma che non trovo documentati sono segnati come osservato; potrebbero non valere su tutte le versioni di iOS. Le voci specifiche della libreria React Native sono segnate come libreria; saltale se scrivi direttamente in Swift.
- I pezzi in gioco
- Entitlement: uno per bundle ID
- L’App Group è l’unico canale
- I token sono opachi, e il selettore
- Misurare l’utilizzo con gli eventi di soglia
- Le programmazioni non partono quando credi
- stopMonitoring fa scattare intervalDidEnd
- Le soglie fantasma di iOS 26
- Uno shield non può aprire la tua app
- Conservare le estensioni personalizzate con prebuild
- Xcode 26, il Simulator e i test
- E su Android
- Checklist
- Risposte rapide
- Perché mi è servito tutto questo
1. I pezzi in gioco
“L’API Screen Time” in realtà sono quattro framework e, in pratica, tre estensioni dell’app:
- FamilyControls: l’autorizzazione (
AuthorizationCenter, con.individualper un’app di autocontrollo) e ilFamilyActivityPicker, che restituisce unaFamilyActivitySelectiondi token di app, categorie e domini web. - ManagedSettings: un
ManagedSettingsStorein cui metti quei token per bloccarli con uno shield. - DeviceActivity: programmazioni ed eventi di soglia sull’utilizzo. Le callback (
intervalDidStart,intervalDidEnd,eventDidReachThresholde le varianti di avviso) vengono consegnate a un’estensioneDeviceActivityMonitor, non alla tua app. - ManagedSettingsUI: un’estensione
ShieldConfigurationDataSourceche dà l’aspetto allo shield e un’estensioneShieldActionDelegateche ne gestisce i pulsanti.
Il punto sono le estensioni. Quando un utente finisce il tempo, di solito la mia app è sospesa o chiusa, quindi è l’estensione di monitoraggio a rimettere su lo shield. Progetta come se il processo della tua app non ci fosse mai quando succedono le cose.
2. Entitlement: una richiesta per bundle ID
L’entitlement Family Controls (Distribution) viene approvato per singolo bundle ID. Con tre estensioni, sono quattro richieste. Le mie erano:
com.honorfit.blocker(l’app)com.honorfit.blocker.ShieldConfigurationcom.honorfit.blocker.ShieldActioncom.honorfit.blocker.ActivityMonitorExtension
Una volta approvate, attiva la capability Family Controls (Distribution) su ogni App ID nel portale sviluppatori, poi crea un App Group e collegalo a tutti e quattro. Se ne dimentichi uno, quel target non ottiene il provisioning. Le approvazioni non sono immediate, quindi invia tutte e quattro le richieste il primo giorno, prima di aver scritto molto codice.
Alla fine il file degli entitlement di ogni estensione è identico:
<key>com.apple.developer.family-controls</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.honorfit.blocker</string>
</array>
Su Expo/EAS, il servizio di build crea i provisioning profile solo per le estensioni che conosce. Io dichiaro ogni target, con i suoi entitlement, sotto extra.eas.build.experimental.ios.appExtensions in 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 è l’unico canale
La tua app e le sue estensioni sono processi separati che non girano quasi mai nello stesso momento. L’unica cosa che condividono è il contenitore dell’App Group, di solito tramite UserDefaults(suiteName:). Tutto quello che serve alle estensioni (la selezione salvata, il testo dello shield, cosa fare quando scatta una soglia) va scritto lì in anticipo.
react-native-device-activity porta questa idea fino in fondo. L’app scrive un elenco di “azioni” sotto una chiave come actions_for_{activity}_{callback}_{event}, e l’estensione di monitoraggio le cerca nel momento in cui scatta la callback. Una conseguenza utile: puoi cambiare cosa fa un monitor già in esecuzione senza riavviarlo. Le estensioni chiamano anche CFPreferencesAppSynchronize prima di leggere, così vedono le ultime scritture dell’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. I token sono opachi, e il selettore
Il selettore consegna alla tua app degli ApplicationToken, ActivityCategoryToken e WebDomainToken, e nient’altro. Per scelta progettuale, l’app non può ritrasformare un token in “Instagram”. Quello che può fare è contarli, quindi la schermata della mia lista di blocco mostra “3 app, 1 categoria”. È tutto qui il modello di privacy, ed è un buon modello: la mia app non sa letteralmente cosa hai bloccato.
FamilyActivityPicker di Apple. L’utente vede nomi e icone delle app; alla tua app tornano solo token opachi.Con l’estensione dello shield è diverso. Lì il sistema ti passa un Application per l’app che viene bloccata, e io ne uso il localizedDisplayName nel testo dello shield. L’app vera e propria vede sempre e solo token.
Due note pratiche:
- Le categorie restano categorie. Se l’utente sceglie “Social”, ricevi un token di categoria, non le app che contiene, a meno che la selezione non sia stata creata con
includeEntireCategory. Io lo tengo afalse, così i conteggi che mostro sono onesti (“1 categoria”, non un numero di app che non so nominare). - libreria Il selettore persistente di react-native-device-activity ricarica la selezione salvata (le spunte quando lo riapri) solo se passi la prop
includeEntireCategory, con un valore qualsiasi. È il suo setter nativo a far partire il caricamento. Senza la prop, il selettore si apre vuoto ogni volta, anche se la selezione era stata salvata.
<DeviceActivitySelectionSheetViewPersisted
familyActivitySelectionId="distracting-apps"
includeEntireCategory={false} // must be present, or nothing preloads
onSelectionChange={(e) => setCounts(e.nativeEvent)}
onDismissRequest={() => setPickerVisible(false)}
/>
libreria Un’altra: blockSelection della libreria aggiunge token a una lista di blocco persistente. Dopo che l’utente modifica la selezione, le app tolte restano bloccate dallo shield. Io chiamo prima resetBlocks() e poi riblocco la selezione attuale, ogni volta.
5. Misurare l’utilizzo con gli eventi di soglia
HonorFit ti dà un saldo di minuti che scende solo mentre un’app bloccata è sullo schermo. DeviceActivity non ha una chiamata del tipo “dimmi quanto l’hanno usata”; ha eventi di soglia che scattano quando l’utilizzo cumulativo di una selezione raggiunge una certa durata. Quindi registro un evento ogni 15 secondi di utilizzo, con il nome in minuti: used-0.25, used-0.5, … fino al saldo. I quarti sono esatti in virgola mobile binaria, quindi i nomi sopravvivono all’andata e ritorno tramite Number(). Sotto la libreria, questa è la semplice API di 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 (il valore predefinito) significa che l’utilizzo precedente alla registrazione degli eventi non conta. È importante per il trucco sulle programmazioni della prossima sezione.
Quando scatta l’ultimo evento, l’estensione di monitoraggio riapplica lo shield e pubblica una notifica di “tempo scaduto”. Ogni evento da 15 secondi aggiorna un’unica notifica con un identificatore fisso, così viene sostituita sul posto e fa da conto alla rovescia approssimativo in tempo reale. interruptionLevel = .passive la consegna in silenzio al Centro Notifiche, senza banner.
libreria Non mettere underscore nei nomi di eventi o attività. La libreria salva gli eventi scattati sotto events_{activity}_{callback}_{eventName} e li rilegge dividendo sul carattere _. Un evento chiamato used_7 torna indietro come used, e non corrisponde mai a niente. Il sintomo era un saldo che non scendeva mai.
6. Le programmazioni non partono quando credi
Se i tuoi eventi di soglia non scattano mai, senza nessun errore, probabilmente è per questo.
“Adesso” è già nel passato
osservato Una DeviceActivitySchedule non ripetuta viene confrontata con dei componenti di data, e il monitor esegue il prossimo intervallo che corrisponde. Se passi l’ora attuale come intervalStart, nel momento in cui il framework la valuta quell’istante è già passato. La prossima corrispondenza è domani, e oggi non scatta niente: niente soglie, niente callback, nessun errore. La soluzione è anticipare l’inizio, così che “adesso” cada dentro l’intervallo:
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);
Anticipare l’inizio non può addebitare all’utente lo scroll fatto prima, grazie a includesPastActivity: false. Inoltre tiene l’intervallo sopra il minimo di Apple: le programmazioni più corte di 15 minuti vengono rifiutate, e questo frega chiunque inizi una sessione poco prima di mezzanotte.
Una programmazione che contiene “adesso” parte subito
osservato Il rovescio della medaglia: una programmazione 00:00–23:59:59 contiene l’ora attuale, quindi parte adesso, non alla prossima mezzanotte. Ho trovato due modi per prenotare domani:
- Una programmazione già scaduta.
[00:00, T), dove T è un minuto prima dell’ora attuale. L’istanza di oggi è finita, quindi il framework prenota quella di domani. La uso per portare oltre la mezzanotte un saldo non speso. repeats: truecon azioni protette. L’istanza di oggi parte subito e non fa niente (ogni azione è protetta da un controllo, vedi la prossima sezione). Quella di domani fa il lavoro vero.
osservato Non usare componenti completi di anno/mese/giorno in una programmazione per puntare a una data precisa. Quando ci ho provato, le callback delle estensioni sono diventate inaffidabili, e coincide con quanto riportato sugli Apple Developer Forums (thread 729841). Limitati a ore/minuti/secondi.
Un’avvertenza: questi comportamenti legati alla mezzanotte si vedono solo su un dispositivo, di notte. Ci ho costruito attorno dei controlli, ma non ho strumentato tante mezzanotti reali quante vorrei, quindi prendi questa sezione come “quello che ho visto”, non come una specifica.
7. stopMonitoring fa scattare intervalDidEnd
osservato Chiamare stopMonitoring, oppure chiamare startMonitoring con il nome di un monitor già in esecuzione, fa sì che iOS chiami intervalDidEnd sulla tua estensione come se la programmazione fosse finita da sola. Se il tuo intervalDidEnd rimette lo shield come rete di sicurezza (il mio lo fa, così un monitor non può scadere e lasciare le app sbloccate), allora ogni volta che l’app ferma un monitor, l’estensione blocca tutto per un istante.
Il sintomo me l’ha segnalato un utente: Spotify si metteva in pausa ogni volta che apriva HonorFit. Fermare il monitor faceva sì che l’estensione bloccasse la selezione con lo shield per un attimo, il che sospendeva Spotify, prima che l’app togliesse di nuovo gli shield.
Due correzioni, servono entrambe. Primo, cancella le azioni dell’estensione prima di fermarla, così non ha niente da eseguire:
function stopActivities(names: string[]): void {
for (const name of names) RNDA.cleanUpAfterActivity(name); // clear actions first
RNDA.stopMonitoring(names); // then stop
}
Secondo, dai a ogni azione di intervalDidEnd e intervalDidStart un timestamp “non prima di” fissato al vero limite della programmazione, meno un minuto di margine. L’estensione lo controlla prima di eseguire qualsiasi cosa:
// 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
}
In Swift puro: salva il vero orario di fine nell’App Group e fai uscire subito intervalDidEnd quando viene chiamata in anticipo.
8. Le soglie fantasma di iOS 26
Su iOS 26.x, eventDidReachThreshold a volte scatta senza nessun utilizzo reale: subito dopo la programmazione, oppure quando un telefono rimasto bloccato per un po’ viene preso in mano o messo in carica. Apple l’ha riconosciuto in alcuni thread dei forum per sviluppatori (809410, 811305, 814559). A quanto pare era stato risolto in una beta di 26.5, ma la gente ha continuato a segnalarlo anche dopo. Nella mia app il sintomo era “tempo finito” dopo che il telefono era stato in tasca, perché l’estensione rimetteva lo shield e il saldo scendeva a zero.
Non puoi impedire all’evento di scattare, ma puoi rifiutarti di crederci. Due fatti sulle soglie vere fanno quasi tutto il lavoro:
- L’utilizzo non può andare più veloce dell’orologio.
used-7non può scattare legittimamente meno di 7 minuti dopo che il monitor è stato armato. Ogni azione di soglia porta con séneverTriggerBefore = armedAt + N minutes − 5s, così l’estensione salta gli scatti impossibilmente precoci. - L’utilizzo vero arriva un po’ alla volta; quello fantasma arriva tutto insieme. Quando l’app rilegge gli eventi scattati, li considera fantasma se la soglia più bassa e quella più alta sono scattate a circa 3 secondi l’una dall’altra ma coprono un minuto o più di utilizzo.
// 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
}
Il controllo 2 gira nell’app, non nell’estensione, quindi una soglia fantasma può comunque far comparire lo shield. Il saldo però si salva, e la volta successiva che si apre l’app lo shield viene tolto di nuovo. Rilevare le raffiche dentro l’estensione richiederebbe codice Swift personalizzato, che non ho ancora scritto.
osservato Un problema correlato va nella direzione opposta: soglie che dovrebbero scattare e non scattano. Con un uso a singhiozzo su iOS 26, ho visto la prima soglia da 15 secondi di un monitor nuovo scattare puntuale, e quelle successive venire accorpate o saltare del tutto (used-0.25 è scattato, used-0.5 e successivi mai). Il mio workaround è considerare scaduto qualsiasi monitor che abbia già misurato dell’utilizzo e riarmarne uno nuovo, sul saldo rimanente, ogni volta che l’app passa in background. Così si perdonano fino a 15 secondi di utilizzo a ogni riavvio, e per me va bene.
9. Uno shield non può aprire la tua app
La soluzione ovvia è un pulsante sullo shield con scritto “Guadagna altro tempo” che apre la tua app. Non c’è un modo supportato per farlo; il DTS di Apple dice che non esiste un’API con cui un’estensione shield possa avviare la sua app principale. Uno ShieldActionDelegate risponde alla pressione di un pulsante con una ShieldActionResponse (.none, .close o .defer), e nessuna di queste apre niente.
osservato Il workaround che troverai in giro (NSExtensionContext().open(url) dall’estensione ShieldAction, che la libreria espone come openUrl) non ha fatto niente sull’iOS attuale quando l’ho provato su un dispositivo a settembre 2026. Il pulsante chiudeva semplicemente lo shield. Anche pubblicare una notifica alla pressione del pulsante è stato inaffidabile, per me.
Quello che uso in produzione: lo shield ha un solo pulsante “Chiudi”, e l’estensione ShieldConfiguration pubblica una notifica locale ogni volta che lo shield viene mostrato. L’utente tocca il banner e finisce nell’app. L’app scrive in anticipo il contenuto della notifica nell’App Group e chiude la notifica quando torna in primo piano. iOS può chiedere la configurazione dello shield più volte per ogni visualizzazione, quindi la pubblicazione ha un 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
}
È un trucco, e dipende dal fatto che l’utente consenta le notifiche, ma è l’unica strada per entrare nell’app che per me abbia funzionato in modo affidabile.
10. Conservare le estensioni personalizzate con prebuild
libreria Se usi Expo, expo prebuild esegue il config plugin di react-native-device-activity, che di default copia in ./targets i sorgenti standard delle estensioni della libreria a ogni esecuzione. La mia ShieldConfiguration personalizzata è stata sostituita senza nessun avviso. La soluzione è un’opzione del plugin:
["react-native-device-activity", {
"appleTeamId": "XXXXXXXXXX",
"appGroup": "group.com.honorfit.blocker",
"copyToTargetFolder": false
}]
Il prezzo: quando aggiorni la libreria, devi confrontare a mano i suoi targets/ e il codice Swift condiviso con i tuoi e riapplicare le tue modifiche.
11. Xcode 26, il Simulator e i test
- Compila con l’SDK di iOS 26. App Store Connect ora rifiuta i caricamenti compilati con un SDK precedente a iOS 26 (ITMS-90725). Su EAS fisso l’immagine di build a
macos-tahoe-26.5-xcode-26.6. - Il Simulator ti porta solo a metà strada. Sui simulatori iOS 26, la richiesta di autorizzazione e il selettore compaiono. Ma l’autorizzazione non si completa mai (“Allow with Passcode”, cioè consenti con il codice, vuole un codice del dispositivo che il Simulator non può impostare), il selettore non ha app da elencare e l’estensione di monitoraggio non parte mai. Tutto quello che c’è nelle sezioni da 5 a 9 richiede un dispositivo fisico.
- Proteggi le chiamate a ManagedSettings controllando l’autorizzazione. Toccare lo store senza autorizzazione faceva comparire una richiesta di codice di sistema sul Simulator, e mi rompeva i test della UI. Controllo lo stato dell’autorizzazione prima di qualsiasi chiamata allo shield.
- Mostra gli errori di autorizzazione nell’interfaccia. I tester di TestFlight non hanno una console. Se
requestAuthorizationfallisce in silenzio, il pulsante sembra solo rotto. Io mostro l’errore e lo stato in un avviso. - Fai test di notte. I miei bug peggiori (app bloccate tutta la notte con tempo ancora disponibile, musica che si metteva in pausa) erano invisibili in code review e si riproducevano solo su un dispositivo, nell’arco di ore.
12. E su Android
Per confronto, Android non ha un equivalente ufficiale, quindi te lo costruisci da solo. Il blocco di HonorFit su Android è un servizio in primo piano (di tipo specialUse, perché il blocco delle app non rientra in nessuno dei tipi elencati) che interroga UsageStatsManager una volta al secondo mentre lo schermo è acceso, per sapere quale app è in primo piano. Quando c’è un’app bloccata e il saldo è a zero, ci disegna sopra una schermata di blocco TYPE_APPLICATION_OVERLAY (“Mostra sopra altre app”). Un receiver di avvio e un allarme watchdog riavviano il servizio dopo i riavvii, gli aggiornamenti dell’app e i gestori della batteria più aggressivi dei produttori.
I compromessi vanno nella direzione opposta rispetto a iOS. Vedi i veri nomi dei pacchetti, quindi la privacy è una promessa che mantieni tu, non una che impone il sistema operativo. Un’app bloccata può restare visibile fino a un intervallo di polling, e sotto l’overlay continua a girare. E ti toccherà compilare le dichiarazioni in Play Console per il tipo di servizio in primo piano, il permesso di overlay e la visibilità dei pacchetti. Ho evitato di proposito un AccessibilityService, che ha la revisione più severa di tutte.
13. Checklist
| Sintomo | Causa probabile | Soluzione |
|---|---|---|
| Il provisioning fallisce per un target | Entitlement o App Group mancante su quel bundle ID | Richiedi Family Controls (Distribution) e collega l’App Group per ogni bundle ID |
| Nessun evento di soglia, mai | intervalStart = adesso, quindi il monitor è prenotato per domani | Anticipa l’inizio; tieni l’intervallo ≥ 15 min |
| Un monitor pensato per domani parte oggi | La programmazione contiene l’ora attuale | Programmazione [00:00, T) già scaduta oggi, oppure repeats: true con azioni protette |
| Le app vengono bloccate per un istante quando apri la tua app | stopMonitoring fa scattare intervalDidEnd | Cancella le azioni prima di fermare; proteggi con un orario “non prima di” |
| “Tempo finito” dopo che il telefono è rimasto inutilizzato | Soglie fantasma di iOS 26 | Controllo sull’orologio più filtro delle raffiche |
| Le soglie successive non scattano mai | Saltate/accorpate con un uso intermittente | Riarma un monitor nuovo a ogni passaggio in background |
| Il pulsante dello shield non apre l’app | Nessuna API supportata | Pubblica una notifica da ShieldConfiguration quando lo shield viene mostrato |
| Gli eventi scattati non corrispondono (RN) | Underscore nel nome dell’evento | Usa used-7, non used_7 |
| Il selettore si apre vuoto (RN) | Manca la prop includeEntireCategory | Passala, con un valore qualsiasi |
14. Risposte rapide
Mi serve l’entitlement Family Controls per ogni estensione?
Sì. Viene concesso per singolo bundle ID. L’app più un’estensione DeviceActivityMonitor, una ShieldConfiguration e una ShieldAction fanno quattro approvazioni, e ogni App ID ha bisogno anche dell’App Group condiviso.
Perché eventDidReachThreshold di DeviceActivity non scatta?
Nel mio caso di solito era una programmazione non ripetuta che partiva da “adesso”, e che quindi veniva prenotata per domani. Anticipa intervalStart. Su iOS 26, con un uso a singhiozzo, saltavano anche le soglie successive sotto il minuto; riarmare un monitor nuovo ha aiutato.
Un’estensione ShieldAction o ShieldConfiguration può aprire l’app principale?
Non con un’API supportata, e nei miei test il workaround con NSExtensionContext non ha fatto niente sull’iOS attuale. Quello che uso in produzione è una notifica locale pubblicata da ShieldConfiguration quando lo shield viene mostrato.
Posso testare l’API Screen Time nel Simulator di iOS?
In parte. Sui simulatori iOS 26 la richiesta e il selettore compaiono, ma l’autorizzazione non si è completata, il selettore non aveva niente da elencare e l’estensione di monitoraggio non è mai partita. Usa un dispositivo.
15. Perché mi è servito tutto questo
Ho creato HonorFit, un’app per bloccare le app in cui il tempo schermo te lo guadagni facendo flessioni. Funziona sulla parola: scrivi le tue ripetizioni, e non ci sono fotocamera, IA o chiamate di rete. Tutto quello che c’è qui sopra è ciò che è servito per far funzionare su iPhone l’idea che “i minuti guadagnati scendono solo mentre un’app bloccata è sullo schermo”. Se vuoi vederla in azione, è sull’App Store e su Google Play. Se sei qui perché un’app del genere vuoi usarla più che svilupparla, ho scritto come funzionano le app che bloccano le app con le flessioni (in inglese) e una panoramica onesta di quelle che ci sono (in inglese), concorrenti compresi. Se ti sei imbattuto in un comportamento di Screen Time che qui non c’è, o hai scoperto che qualcosa di quanto sopra è sbagliato su un iOS più recente, mi piacerebbe saperlo.