Die iOS Screen Time API 2026: Was Apples Dokumentation dir nicht sagt
Im August und September 2026 habe ich einen App-Blocker für iOS fertiggestellt und veröffentlicht. Er nutzt die Bildschirmzeit-Frameworks (FamilyControls, DeviceActivity, ManagedSettings und ManagedSettingsUI) aus React Native heraus, über Expo SDK 57 und die Bibliothek react-native-device-activity (0.6.1). Apples Dokumentation erklärt dir, was jeder Typ ist. Wie sie sich auf einem echten Handy verhalten, über Nacht, unter iOS 26, steht da kaum.
Das hier ist die Liste, die ich gern vorher gehabt hätte. Verhalten, das ich auf einem Gerät gesehen, aber nirgends dokumentiert gefunden habe, ist mit beobachtet markiert; es gilt vielleicht nicht für jede iOS-Version. Punkte, die nur die React-Native-Bibliothek betreffen, sind mit Bibliothek markiert; die kannst du überspringen, wenn du direkt in Swift schreibst.
- Die beweglichen Teile
- Entitlements: eins pro Bundle-ID
- Die App Group ist der einzige Kanal
- Tokens sind undurchsichtig, und die Auswahl
- Nutzung mit Threshold-Events messen
- Zeitpläne starten nicht, wann du denkst
- stopMonitoring löst intervalDidEnd aus
- Phantom-Thresholds unter iOS 26
- Ein Shield kann deine App nicht öffnen
- Eigene Extensions durch prebuild retten
- Xcode 26, der Simulator und Testen
- Und auf Android
- Checkliste
- Kurze Antworten
- Wozu ich das alles brauchte
1. Die beweglichen Teile
„Die Screen Time API“ besteht aus vier Frameworks und in der Praxis drei App-Extensions:
- FamilyControls: die Autorisierung (
AuthorizationCenter, mit.individualfür eine App zur Selbstkontrolle) und derFamilyActivityPicker, der eineFamilyActivitySelectionaus App-, Kategorie- und Web-Domain-Tokens zurückgibt. - ManagedSettings: ein
ManagedSettingsStore, in den du diese Tokens steckst, um sie mit einem Shield zu sperren. - DeviceActivity: Zeitpläne und Events für Nutzungsschwellen (Thresholds). Die Callbacks (
intervalDidStart,intervalDidEnd,eventDidReachThresholdund die Warning-Varianten) werden an eineDeviceActivityMonitor-Extension zugestellt, nicht an deine App. - ManagedSettingsUI: eine
ShieldConfigurationDataSource-Extension, die das Shield gestaltet, und eineShieldActionDelegate-Extension, die seine Buttons behandelt.
Die Extensions sind der eigentliche Kern. Meine App ist meistens suspendiert oder beendet, wenn einem Nutzer die Zeit ausgeht, also ist es die Monitor-Extension, die das Shield wieder hochzieht. Plane so, als wäre dein App-Prozess nie da, wenn etwas passiert.
2. Entitlements: eine Anfrage pro Bundle-ID
Das Entitlement Family Controls (Distribution) wird pro Bundle-ID genehmigt. Mit drei Extensions sind das vier Anfragen. Meine waren:
com.honorfit.blocker(die App)com.honorfit.blocker.ShieldConfigurationcom.honorfit.blocker.ShieldActioncom.honorfit.blocker.ActivityMonitorExtension
Sobald sie genehmigt sind, aktivierst du die Capability Family Controls (Distribution) für jede App-ID im Entwicklerportal, legst dann eine App Group an und hängst sie an alle vier. Vergisst du eine, lässt sich dieses Target nicht provisionieren. Die Genehmigungen kommen nicht sofort, also stell alle vier Anfragen am ersten Tag, bevor du viel Code geschrieben hast.
Die Entitlements-Datei jeder Extension sieht am Ende gleich aus:
<key>com.apple.developer.family-controls</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.honorfit.blocker</string>
</array>
Bei Expo/EAS erstellt der Build-Service nur Provisioning-Profile für Extensions, die er kennt. Ich deklariere jedes Target samt Entitlements unter extra.eas.build.experimental.ios.appExtensions in der 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. Die App Group ist der einzige Kanal
Deine App und ihre Extensions sind getrennte Prozesse, die fast nie gleichzeitig laufen. Das Einzige, was sie teilen, ist der App-Group-Container, meist als UserDefaults(suiteName:). Alles, was die Extensions brauchen (die gespeicherte Auswahl, den Shield-Text, was passieren soll, wenn ein Threshold feuert), muss vorher dort hineingeschrieben werden.
react-native-device-activity treibt das auf die Spitze. Die App schreibt eine Liste von „Actions“ unter einen Schlüssel wie actions_for_{activity}_{callback}_{event}, und die Monitor-Extension schlägt sie nach, wenn der Callback feuert. Eine nützliche Folge: Du kannst ändern, was ein bereits laufender Monitor tut, ohne ihn neu zu starten. Die Extensions rufen außerdem vor dem Lesen CFPreferencesAppSynchronize auf, damit sie die neuesten Schreibvorgänge der App sehen:
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. Tokens sind undurchsichtig, und die Auswahl
Die Auswahl gibt deiner App ApplicationTokens, ActivityCategoryTokens und WebDomainTokens, sonst nichts. Absichtlich kann die App ein Token nicht zurück in „Instagram“ verwandeln. Zählen kann sie sie aber, deshalb zeigt mein Sperrlisten-Bildschirm „3 Apps, 1 Kategorie“. Das ist das ganze Datenschutzmodell, und es ist ein gutes: Meine App weiß buchstäblich nicht, was du gesperrt hast.
FamilyActivityPicker. Der Nutzer sieht App-Namen und Symbole; deine App bekommt nur undurchsichtige Tokens zurück.Bei der Shield-Extension ist es anders. Dort übergibt dir das System eine Application für die gesperrte App, und ich nutze ihren localizedDisplayName im Shield-Text. Die App selbst sieht immer nur Tokens.
Zwei praktische Hinweise:
- Kategorien bleiben Kategorien. Wenn der Nutzer „Soziale Netzwerke“ auswählt, bekommst du ein Kategorie-Token, nicht die Apps darin, außer die Auswahl wurde mit
includeEntireCategoryerstellt. Ich lasse es auffalse, damit die angezeigten Zahlen ehrlich sind („1 Kategorie“, nicht eine Anzahl von Apps, die ich nicht benennen kann). - Bibliothek Der persistierte Picker von react-native-device-activity lädt die gespeicherte Auswahl (die Häkchen beim erneuten Öffnen) nur vor, wenn du die Prop
includeEntireCategoryübergibst, mit irgendeinem Wert. Erst ihr nativer Setter stößt das Laden an. Ohne die Prop öffnet sich die Auswahl jedes Mal leer, obwohl die Auswahl gespeichert wurde.
<DeviceActivitySelectionSheetViewPersisted
familyActivitySelectionId="distracting-apps"
includeEntireCategory={false} // must be present, or nothing preloads
onSelectionChange={(e) => setCounts(e.nativeEvent)}
onDismissRequest={() => setPickerVisible(false)}
/>
Bibliothek Noch etwas: blockSelection der Bibliothek fügt Tokens zu einer persistierten Sperrliste hinzu. Nachdem der Nutzer seine Auswahl bearbeitet hat, bleiben die entfernten Apps gesperrt. Ich rufe jedes Mal zuerst resetBlocks() auf und sperre dann die aktuelle Auswahl neu.
5. Nutzung mit Threshold-Events messen
HonorFit gibt dir ein Guthaben an Minuten, das nur abläuft, solange eine gesperrte App auf dem Bildschirm ist. DeviceActivity hat keinen Aufruf nach dem Motto „sag mir, wie lange sie es genutzt haben“; es hat Threshold-Events, die feuern, sobald die kumulierte Nutzung einer Auswahl eine bestimmte Dauer erreicht. Also registriere ich ein Event pro 15 Sekunden Nutzung, benannt in Minuten: used-0.25, used-0.5, … bis zum Guthaben. Viertel sind im binären Gleitkommaformat exakt darstellbar, also überstehen die Namen den Weg durch Number() unverändert. Unter der Bibliothek ist das die ganz normale Apple-API:
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 (der Standard) bedeutet, dass Nutzung von vor der Registrierung der Events nicht zählt. Das ist wichtig für den Zeitplan-Trick im nächsten Abschnitt.
Wenn das letzte Event feuert, setzt die Monitor-Extension das Shield wieder und postet eine „Zeit ist um“-Mitteilung. Jedes 15-Sekunden-Event aktualisiert eine einzige Mitteilung mit fester ID, sie wird also an Ort und Stelle ersetzt und funktioniert als grober Live-Countdown. interruptionLevel = .passive stellt sie still in die Mitteilungszentrale, ohne Banner.
Bibliothek Keine Unterstriche in Event- oder Activity-Namen. Die Bibliothek speichert gefeuerte Events unter events_{activity}_{callback}_{eventName} und zerlegt sie beim Zurücklesen an _. Ein Event namens used_7 kommt als used zurück, und nichts passt je zusammen. Das Symptom war ein Guthaben, das nie weniger wurde.
6. Zeitpläne starten nicht, wann du denkst
Wenn deine Threshold-Events stillschweigend nie feuern, liegt es wahrscheinlich daran.
„Jetzt“ liegt schon in der Vergangenheit
beobachtet Ein nicht wiederholter DeviceActivitySchedule wird mit Datumskomponenten abgeglichen, und der Monitor läuft im nächsten passenden Intervall. Übergibst du die aktuelle Uhrzeit als intervalStart, ist dieser Moment schon vorbei, bis das Framework ihn auswertet. Der nächste Treffer ist morgen, und heute feuert nichts: keine Thresholds, keine Callbacks, kein Fehler. Die Lösung ist, den Start zurückzudatieren, sodass „jetzt“ im Intervall liegt:
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);
Wegen includesPastActivity: false kann das Zurückdatieren dem Nutzer kein früheres Scrollen anrechnen. Außerdem hält es das Intervall über Apples Minimum: Zeitpläne unter 15 Minuten werden abgelehnt, und das trifft jeden, der kurz vor Mitternacht eine Session startet.
Ein Zeitplan, der „jetzt“ enthält, startet sofort
beobachtet Die Kehrseite: Ein Zeitplan von 00:00 bis 23:59:59 enthält die aktuelle Uhrzeit, startet also jetzt und nicht erst um die nächste Mitternacht. Zwei Wege, die ich gefunden habe, um für morgen zu planen:
- Ein bereits abgelaufener Zeitplan.
[00:00, T), wobei T eine Minute vor der aktuellen Tageszeit liegt. Die heutige Instanz ist vorbei, also plant das Framework die von morgen ein. So trage ich ein nicht verbrauchtes Guthaben über Mitternacht. repeats: truemit abgesicherten Actions. Die heutige Instanz startet sofort und tut nichts (jede Action ist abgesichert, siehe nächster Abschnitt). Die von morgen macht die eigentliche Arbeit.
beobachtet Nutze keine vollständigen Jahr/Monat/Tag-Komponenten in einem Zeitplan, um ein bestimmtes Datum anzusteuern. Als ich das versucht habe, wurden die Extension-Callbacks unzuverlässig, was zu Berichten in den Apple Developer Forums passt (Thread 729841). Bleib bei Stunde/Minute/Sekunde.
Eine Einschränkung: Dieses Verhalten um Mitternacht zeigt sich nur auf einem Gerät, über Nacht. Ich habe Schutzmechanismen darum gebaut, aber nicht so viele echte Mitternächte protokolliert, wie ich gern hätte. Betrachte diesen Abschnitt also als „was ich gesehen habe“, nicht als Spezifikation.
7. stopMonitoring löst intervalDidEnd aus
beobachtet Ruft man stopMonitoring auf, oder startMonitoring mit dem Namen eines Monitors, der schon läuft, ruft iOS intervalDidEnd in deiner Extension auf, als wäre der Zeitplan regulär zu Ende gegangen. Wenn dein intervalDidEnd als Sicherheitsnetz wieder sperrt (meins tut das, damit ein Monitor nicht ablaufen und Apps ungesperrt lassen kann), dann sperrt die Extension jedes Mal, wenn die App einen Monitor stoppt, kurz alles.
Ein Nutzer hat das Symptom gemeldet: Spotify pausierte jedes Mal, wenn er HonorFit öffnete. Das Stoppen des Monitors brachte die Extension dazu, die Auswahl kurz zu sperren, was Spotify anhielt, bevor die App die Shields wieder aufhob.
Zwei Korrekturen, beide nötig. Erstens: Lösch die Actions der Extension vor dem Stoppen, damit sie nichts mehr auszuführen hat:
function stopActivities(names: string[]): void {
for (const name of names) RNDA.cleanUpAfterActivity(name); // clear actions first
RNDA.stopMonitoring(names); // then stop
}
Zweitens: Gib jeder intervalDidEnd- und intervalDidStart-Action einen „nicht vor“-Zeitstempel an der echten Grenze des Zeitplans, minus eine Minute Puffer. Die Extension prüft ihn, bevor sie irgendetwas ausführt:
// 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 reinem Swift: Speichere die echte Endzeit in der App Group und lass intervalDidEnd abbrechen, wenn es zu früh aufgerufen wird.
8. Phantom-Thresholds unter iOS 26
Unter iOS 26.x feuert eventDidReachThreshold manchmal ganz ohne echte Nutzung: direkt nach dem Einplanen oder wenn ein Handy, das eine Weile gesperrt herumlag, hochgenommen oder ans Ladekabel gehängt wird. Apple hat das in Threads im Entwicklerforum bestätigt (809410, 811305, 814559). Angeblich wurde es in einer Beta von 26.5 behoben, aber danach haben Leute es weiter gemeldet. In meiner App war das Symptom „keine Zeit mehr“, nachdem das Handy in der Hosentasche war, weil die Extension wieder gesperrt hatte und das Guthaben auf null gefallen war.
Du kannst nicht verhindern, dass das Event feuert, aber du kannst ihm den Glauben verweigern. Zwei Tatsachen über echte Thresholds erledigen den Großteil:
- Nutzung kann die Uhr nicht überholen.
used-7kann legitimerweise nicht früher als 7 Minuten nach dem Scharfschalten des Monitors feuern. Jede Threshold-Action trägtneverTriggerBefore = armedAt + N minutes − 5s, sodass die Extension unmöglich frühe Auslösungen überspringt. - Echte Nutzung kommt schrittweise, Phantome kommen als Schwall. Wenn die App die gefeuerten Events zurückliest, behandelt sie sie als Phantom, wenn der niedrigste und der höchste Threshold innerhalb von etwa 3 Sekunden gefeuert haben, aber eine Minute Nutzung oder mehr auseinanderliegen.
// 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
}
Guard 2 läuft in der App, nicht in der Extension, ein Phantom kann also trotzdem das Shield hochziehen. Das Guthaben bleibt aber erhalten, und beim nächsten Öffnen der App hebt sie das Shield wieder auf. Die Schwall-Erkennung in der Extension zu machen, würde eigenen Swift-Code brauchen, den ich noch nicht geschrieben habe.
beobachtet Ein verwandtes Problem geht in die andere Richtung: Thresholds, die feuern sollten, und es nicht tun. Bei Rein-und-raus-Nutzung unter iOS 26 habe ich gesehen, dass der erste 15-Sekunden-Threshold eines frischen Monitors pünktlich feuert und spätere zusammengefasst werden oder ganz wegfallen (used-0.25 feuerte, used-0.5 und alles danach nie). Mein Workaround: Jeder Monitor, der schon Nutzung gemessen hat, gilt als veraltet, und jedes Mal, wenn die App in den Hintergrund geht, starte ich einen frischen über das restliche Guthaben. Das schenkt dem Nutzer pro Neustart bis zu 15 Sekunden Nutzung, und damit kann ich leben.
9. Ein Shield kann deine App nicht öffnen
Das naheliegende Design ist ein Shield-Button mit „Mehr Zeit verdienen“, der deine App öffnet. Dafür gibt es keinen unterstützten Weg; laut Apple DTS gibt es keine API, mit der eine Shield-Extension ihre Haupt-App starten kann. Ein ShieldActionDelegate beantwortet einen Button-Tipp mit einer ShieldActionResponse (.none, .close oder .defer), und keine davon öffnet irgendetwas.
beobachtet Der Workaround, den du finden wirst (NSExtensionContext().open(url) aus der ShieldAction-Extension, den die Bibliothek als openUrl anbietet), hat unter aktuellem iOS nichts bewirkt, als ich ihn im September 2026 auf einem Gerät getestet habe. Der Button hat einfach das Shield geschlossen. Eine Mitteilung beim Button-Tipp zu posten, war bei mir auch unzuverlässig.
Was ich stattdessen ausliefere: Das Shield hat einen einzigen Button „Schließen“, und die ShieldConfiguration-Extension postet eine lokale Mitteilung, sobald das Shield angezeigt wird. Der Nutzer tippt auf das Banner und landet in der App. Die App schreibt den Inhalt der Mitteilung vorab in die App Group und entfernt die Mitteilung, wenn sie in den Vordergrund kommt. iOS fragt die Shield-Konfiguration pro Anzeige eventuell mehrmals ab, deshalb ist das Posten entprellt:
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 ist ein Hack, und er setzt voraus, dass der Nutzer Mitteilungen erlaubt, aber es ist der einzige Weg in die App, der bei mir zuverlässig funktioniert hat.
10. Eigene Extensions durch prebuild retten
Bibliothek Wenn du Expo nutzt, führt expo prebuild das Config-Plugin von react-native-device-activity aus, das standardmäßig bei jedem Lauf die mitgelieferten Extension-Quellen der Bibliothek nach ./targets kopiert. Meine angepasste ShieldConfiguration wurde stillschweigend überschrieben. Die Lösung ist eine einzige Plugin-Option:
["react-native-device-activity", {
"appleTeamId": "XXXXXXXXXX",
"appGroup": "group.com.honorfit.blocker",
"copyToTargetFolder": false
}]
Der Preis: Wenn du die Bibliothek aktualisierst, musst du ihre targets/ und den gemeinsamen Swift-Code von Hand mit deinen vergleichen und deine Änderungen neu einspielen.
11. Xcode 26, der Simulator und Testen
- Mit dem iOS-26-SDK bauen. App Store Connect lehnt inzwischen Uploads ab, die mit einem SDK älter als iOS 26 gebaut wurden (ITMS-90725). Auf EAS pinne ich das Build-Image auf
macos-tahoe-26.5-xcode-26.6. - Der Simulator bringt dich nur ein Stück weit. In Simulatoren mit iOS 26 erscheinen die Autorisierungsabfrage und die Auswahl zwar. Aber die Autorisierung wird nie abgeschlossen („Allow with Passcode“ verlangt einen Gerätecode, den der Simulator nicht setzen kann), die Auswahl hat keine Apps zum Auflisten, und die Monitor-Extension läuft nie. Alles aus den Abschnitten 5 bis 9 braucht ein echtes Gerät.
- ManagedSettings-Aufrufe an die Autorisierung koppeln. Den Store ohne Autorisierung anzufassen, hat im Simulator eine System-Abfrage nach dem Code aufpoppen lassen, was meine UI-Tests kaputt gemacht hat. Ich prüfe den Autorisierungsstatus vor jedem Shield-Aufruf.
- Autorisierungsfehler in der UI anzeigen. TestFlight-Tester haben keine Konsole. Wenn
requestAuthorizationstillschweigend fehlschlägt, wirkt der Button einfach kaputt. Ich zeige Fehler und Status in einem Hinweisfenster an. - Über Nacht testen. Meine schlimmsten Bugs (die ganze Nacht gesperrt trotz Restzeit, pausierende Musik) waren im Code-Review unsichtbar und ließen sich nur auf einem Gerät über Stunden reproduzieren.
12. Und auf Android
Zum Vergleich: Android hat kein offizielles Gegenstück, also baust du es selbst. Der Android-Blocker von HonorFit ist ein Vordergrunddienst (Typ specialUse, weil App-Sperren zu keinem der aufgeführten Typen passt), der bei eingeschaltetem Bildschirm einmal pro Sekunde UsageStatsManager abfragt, um zu erfahren, welche App vorne ist. Ist eine gesperrte App offen und das Guthaben null, zeichnet er einen TYPE_APPLICATION_OVERLAY-Sperrbildschirm darüber („Über anderen Apps einblenden“). Ein Boot-Receiver und ein Watchdog-Alarm starten den Dienst nach Neustarts, App-Updates und aggressiven Akku-Managern der Hersteller neu.
Die Kompromisse zeigen in die umgekehrte Richtung als bei iOS. Du siehst echte Paketnamen, Datenschutz ist also ein Versprechen, das du hältst, nicht eins, das das Betriebssystem erzwingt. Eine gesperrte App kann bis zu einem Abfrageintervall lang sichtbar sein, und sie läuft unter dem Overlay weiter. Und du schreibst Play-Console-Erklärungen für den Typ des Vordergrunddiensts, die Overlay-Berechtigung und die Paketsichtbarkeit. Einen AccessibilityService habe ich bewusst vermieden, denn der wird von allen am strengsten geprüft.
13. Checkliste
| Symptom | Wahrscheinliche Ursache | Lösung |
|---|---|---|
| Provisioning schlägt für ein Target fehl | Entitlement oder App Group fehlt für diese Bundle-ID | Family Controls (Distribution) beantragen und die App Group für jede Bundle-ID anhängen |
| Nie irgendwelche Threshold-Events | intervalStart = jetzt, also ist der Monitor für morgen eingeplant | Start zurückdatieren; Intervall ≥ 15 Min. halten |
| Monitor für morgen läuft schon heute | Zeitplan enthält die aktuelle Uhrzeit | Heute schon abgelaufener [00:00, T)-Zeitplan oder repeats: true mit abgesicherten Actions |
| Apps werden kurz gesperrt, wenn deine App sich öffnet | stopMonitoring löst intervalDidEnd aus | Actions vor dem Stoppen löschen; mit „nicht vor“-Zeit absichern |
| „Keine Zeit mehr“, nachdem das Handy herumlag | Phantom-Thresholds unter iOS 26 | Uhrzeit-Guard plus Schwall-Filter |
| Spätere Thresholds feuern nie | Bei unregelmäßiger Nutzung verworfen/zusammengefasst | Bei jedem Wechsel in den Hintergrund einen frischen Monitor starten |
| Shield-Button öffnet die App nicht | Keine unterstützte API | Beim Anzeigen eine Mitteilung aus ShieldConfiguration posten |
| Gefeuerte Events passen nicht (RN) | Unterstrich im Event-Namen | used-7 nutzen, nicht used_7 |
| Auswahl öffnet sich leer (RN) | Prop includeEntireCategory fehlt | Übergeben, mit beliebigem Wert |
14. Kurze Antworten
Brauche ich das Family-Controls-Entitlement für jede Extension?
Ja. Es wird pro Bundle-ID vergeben. Die App plus eine DeviceActivityMonitor-, eine ShieldConfiguration- und eine ShieldAction-Extension sind vier Genehmigungen, und jede App-ID braucht außerdem die gemeinsame App Group.
Warum feuert eventDidReachThreshold von DeviceActivity nicht?
Bei mir war es meistens ein nicht wiederholter Zeitplan, der „jetzt“ startet und dadurch für morgen eingeplant wird. Datier intervalStart zurück. Unter iOS 26 sind bei Rein-und-raus-Nutzung außerdem spätere Thresholds unter einer Minute verloren gegangen; einen frischen Monitor zu starten hat geholfen.
Kann eine ShieldAction- oder ShieldConfiguration-Extension die Haupt-App öffnen?
Nicht mit einer unterstützten API, und der NSExtensionContext-Workaround hat in meinen Tests unter aktuellem iOS nichts bewirkt. Was ich ausliefere, ist eine lokale Mitteilung, die ShieldConfiguration postet, sobald das Shield angezeigt wird.
Kann ich die Screen Time API im iOS-Simulator testen?
Teilweise. Abfrage und Auswahl erscheinen in Simulatoren mit iOS 26, aber die Autorisierung wurde nicht abgeschlossen, die Auswahl hatte nichts aufzulisten, und die Monitor-Extension lief nie. Nimm ein Gerät.
15. Wozu ich das alles brauchte
Ich habe HonorFit gebaut, einen App-Blocker, bei dem du dir Bildschirmzeit mit Liegestützen verdienst. Er läuft auf dem Ehrensystem: Du trägst deine Wiederholungen ein, und es gibt keine Kamera, keine KI und keine Netzwerkaufrufe. Alles oben ist das, was nötig war, damit „deine verdienten Minuten laufen nur ab, solange eine gesperrte App auf dem Bildschirm ist“ auf einem iPhone funktioniert. Wenn du es in Aktion sehen willst: Es ist im App Store und bei Google Play. Falls du hier bist, weil du so eine App willst, statt eine zu bauen: Ich habe aufgeschrieben, wie Liegestütz-App-Blocker funktionieren, und einen ehrlichen Überblick über die verfügbaren Apps, Konkurrenz eingeschlossen. Wenn du auf ein Bildschirmzeit-Verhalten gestoßen bist, das hier fehlt, oder festgestellt hast, dass etwas oben unter einem neueren iOS nicht mehr stimmt, würde ich gern davon hören.