Screen Time API w iOS w 2026 roku: czego nie powie ci dokumentacja Apple
Sierpień i wrzesień 2026 spędziłem na wypuszczaniu blokady aplikacji na iOS. Korzysta z frameworków Czasu przed ekranem (FamilyControls, DeviceActivity, ManagedSettings i ManagedSettingsUI) z poziomu React Native, przez Expo SDK 57 i bibliotekę react-native-device-activity (0.6.1). Dokumentacja Apple mówi, czym jest każdy typ. Niewiele mówi o tym, jak zachowują się na prawdziwym telefonie, przez noc, na iOS 26.
To lista, którą sam chciałbym mieć na starcie. Zachowania, które widziałem na urządzeniu, ale nie znalazłem w dokumentacji, oznaczam jako zaobserwowane; nie muszą się potwierdzać na każdej wersji iOS. Punkty dotyczące tylko biblioteki React Native oznaczam jako biblioteka; pomiń je, jeśli piszesz bezpośrednio w Swifcie.
- Z czego to się składa
- Uprawnienia: jedno na bundle ID
- App Group to jedyny kanał
- Tokeny są nieprzezroczyste, a do tego okno wyboru
- Mierzenie użycia zdarzeniami progowymi
- Harmonogramy nie startują wtedy, kiedy myślisz
- stopMonitoring wywołuje intervalDidEnd
- Widmowe progi w iOS 26
- Osłona nie może otworzyć twojej aplikacji
- Jak zachować własne rozszerzenia przy prebuild
- Xcode 26, Symulator i testowanie
- A na Androidzie
- Lista kontrolna
- Krótkie odpowiedzi
- Po co mi to wszystko
1. Z czego to się składa
„Screen Time API” to cztery frameworki i w praktyce trzy rozszerzenia aplikacji:
- FamilyControls: autoryzacja (
AuthorizationCenter, z.individualw przypadku aplikacji do samokontroli) iFamilyActivityPicker, który zwracaFamilyActivitySelectionz tokenami aplikacji, kategorii i domen. - ManagedSettings:
ManagedSettingsStore, do którego wkładasz te tokeny, żeby objąć je osłoną (shield). - DeviceActivity: harmonogramy i zdarzenia progowe użycia. Wywołania zwrotne (
intervalDidStart,intervalDidEnd,eventDidReachThresholdi ich warianty z ostrzeżeniem) trafiają do rozszerzeniaDeviceActivityMonitor, a nie do twojej aplikacji. - ManagedSettingsUI: rozszerzenie
ShieldConfigurationDataSource, które nadaje osłonie wygląd, i rozszerzenieShieldActionDelegate, które obsługuje jej przyciski.
Cała rzecz kręci się wokół rozszerzeń. Kiedy użytkownikowi kończy się czas, moja aplikacja jest zwykle wstrzymana albo zamknięta, więc to rozszerzenie monitora z powrotem stawia osłonę. Projektuj tak, jakby procesu twojej aplikacji nigdy nie było w chwili, gdy coś się dzieje.
2. Uprawnienia: jeden wniosek na bundle ID
Uprawnienie (entitlement) Family Controls (Distribution) jest zatwierdzane dla każdego bundle ID osobno. Przy trzech rozszerzeniach to cztery wnioski. Moje były takie:
com.honorfit.blocker(aplikacja)com.honorfit.blocker.ShieldConfigurationcom.honorfit.blocker.ShieldActioncom.honorfit.blocker.ActivityMonitorExtension
Kiedy zostaną zatwierdzone, włącz capability Family Controls (Distribution) na każdym App ID w portalu dewelopera, a potem utwórz jedną App Group i podepnij ją do wszystkich czterech. Pomiń jeden i ten target się nie sprovisionuje. Zatwierdzenie nie przychodzi od razu, więc złóż wszystkie cztery wnioski pierwszego dnia, zanim napiszesz dużo kodu.
Plik entitlements każdego rozszerzenia wygląda w końcu tak samo:
<key>com.apple.developer.family-controls</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.honorfit.blocker</string>
</array>
W Expo/EAS serwis budujący tworzy profile provisioningowe tylko dla rozszerzeń, o których wie. Każdy target, razem z jego uprawnieniami, deklaruję w extra.eas.build.experimental.ios.appExtensions w pliku 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. App Group to jedyny kanał
Twoja aplikacja i jej rozszerzenia to osobne procesy, które prawie nigdy nie działają w tym samym czasie. Jedyne, co dzielą, to kontener App Group, zwykle w postaci UserDefaults(suiteName:). Wszystko, czego potrzebują rozszerzenia (zapisany wybór, tekst osłony, co zrobić, gdy wywoła się próg), trzeba tam zapisać zawczasu.
react-native-device-activity doprowadza to do końca. Aplikacja zapisuje listę „akcji” pod kluczem w stylu actions_for_{activity}_{callback}_{event}, a rozszerzenie monitora odczytuje je w momencie wywołania. Przydatna konsekwencja: możesz zmienić to, co robi już działający monitor, bez jego restartowania. Rozszerzenia wywołują też przed odczytem CFPreferencesAppSynchronize, żeby widzieć najnowsze zapisy aplikacji:
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. Tokeny są nieprzezroczyste, a do tego okno wyboru
Okno wyboru przekazuje twojej aplikacji ApplicationTokeny, ActivityCategoryTokeny i WebDomainTokeny, i nic poza tym. Z założenia aplikacja nie może zamienić tokena z powrotem na „Instagram”. Może je za to policzyć, więc mój ekran listy blokowanych pokazuje „3 aplikacje, 1 kategoria”. Na tym polega cały model prywatności, i to dobry model: moja aplikacja dosłownie nie wie, co zablokowałeś.
FamilyActivityPicker od Apple. Użytkownik widzi nazwy i ikony aplikacji; twoja aplikacja dostaje z powrotem tylko nieprzezroczyste tokeny.Rozszerzenie osłony to inna historia. Tam system przekazuje ci Application dla aplikacji, która jest blokowana, a ja używam jej localizedDisplayName w tekście osłony. Sama aplikacja widzi tylko tokeny.
Dwie praktyczne uwagi:
- Kategorie zostają kategoriami. Jeśli użytkownik wybierze „Społecznościowe”, dostajesz token kategorii, a nie aplikacje w środku, chyba że wybór utworzono z
includeEntireCategory. Trzymam to nafalse, żeby pokazywane liczby były uczciwe („1 kategoria”, a nie liczba aplikacji, których nie umiem nazwać). - biblioteka Zapamiętywane okno wyboru z react-native-device-activity wczytuje zapisany wybór (ptaszki po ponownym otwarciu) tylko wtedy, gdy przekażesz prop
includeEntireCategory, z dowolną wartością. To jego natywny setter uruchamia wczytywanie. Bez tego propa okno wyboru za każdym razem otwiera się puste, mimo że wybór został zapisany.
<DeviceActivitySelectionSheetViewPersisted
familyActivitySelectionId="distracting-apps"
includeEntireCategory={false} // must be present, or nothing preloads
onSelectionChange={(e) => setCounts(e.nativeEvent)}
onDismissRequest={() => setPickerVisible(false)}
/>
biblioteka Jeszcze jedno: blockSelection z tej biblioteki dodaje tokeny do zapamiętanej listy blokowanych. Kiedy użytkownik zmieni swój wybór, usunięte aplikacje nadal są pod osłoną. Dlatego za każdym razem najpierw wywołuję resetBlocks(), a potem blokuję od nowa aktualny wybór.
5. Mierzenie użycia zdarzeniami progowymi
HonorFit daje ci saldo minut, które schodzi tylko wtedy, gdy zablokowana aplikacja jest na ekranie. DeviceActivity nie ma wywołania „powiedz mi, jak długo tego używali”; ma zdarzenia progowe, które wywołują się, gdy łączne użycie wybranych aplikacji osiągnie określony czas. Rejestruję więc jedno zdarzenie na każde 15 sekund użycia, nazwane w minutach: used-0.25, used-0.5, … aż do wysokości salda. Ćwiartki są dokładnie reprezentowalne w binarnej liczbie zmiennoprzecinkowej, więc nazwy bez strat przechodzą w obie strony przez Number(). Pod biblioteką to zwykłe API 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 (wartość domyślna) oznacza, że użycie sprzed zarejestrowania zdarzeń się nie liczy. To ważne przy sztuczce z harmonogramem z następnej sekcji.
Kiedy wywoła się ostatnie zdarzenie, rozszerzenie monitora z powrotem zakłada osłonę i wysyła powiadomienie „czas minął”. Każde 15-sekundowe zdarzenie aktualizuje jedno powiadomienie ze stałym identyfikatorem, więc podmienia się ono w miejscu i działa jak przybliżone odliczanie na żywo. interruptionLevel = .passive dostarcza je po cichu do Centrum powiadomień, bez banera.
biblioteka Nie używaj podkreśleń w nazwach zdarzeń ani aktywności. Biblioteka zapisuje wywołane zdarzenia pod events_{activity}_{callback}_{eventName} i odczytuje je z powrotem, dzieląc po _. Zdarzenie o nazwie used_7 wraca jako used i nic nigdy nie pasuje. Objaw: saldo, które nigdy nie maleje.
6. Harmonogramy nie startują wtedy, kiedy myślisz
Jeśli twoje zdarzenia progowe po cichu nigdy się nie wywołują, to prawdopodobnie właśnie to.
„Teraz” jest już w przeszłości
zaobserwowane Niepowtarzalny DeviceActivitySchedule jest dopasowywany do komponentów daty, a monitor uruchamia najbliższy pasujący przedział. Jeśli jako intervalStart przekażesz bieżącą godzinę, to zanim framework ją oceni, ta chwila już minie. Najbliższe dopasowanie wypada jutro, a dziś nic się nie wywołuje: żadnych progów, żadnych wywołań zwrotnych, żadnego błędu. Rozwiązanie to cofnięcie początku, tak żeby „teraz” mieściło się w przedziale:
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);
Cofnięcie początku nie może obciążyć użytkownika za wcześniejsze scrollowanie, dzięki includesPastActivity: false. Utrzymuje też przedział powyżej minimum Apple: harmonogramy krótsze niż 15 minut są odrzucane, co daje się we znaki każdemu, kto zaczyna sesję tuż przed północą.
Harmonogram, który obejmuje „teraz”, startuje od razu
zaobserwowane Druga strona medalu: harmonogram 00:00–23:59:59 obejmuje bieżącą godzinę, więc startuje teraz, a nie o najbliższej północy. Znalazłem dwa sposoby, żeby zaplanować coś na jutro:
- Harmonogram, który już wygasł.
[00:00, T), gdzie T to minuta przed bieżącą godziną. Dzisiejsza instancja już się skończyła, więc framework planuje jutrzejszą. Używam tego, żeby przenieść niewykorzystane saldo przez północ. repeats: truez zabezpieczonymi akcjami. Dzisiejsza instancja startuje od razu i nic nie robi (każda akcja jest zabezpieczona, patrz następna sekcja). Prawdziwą robotę wykonuje jutrzejsza.
zaobserwowane Nie używaj pełnych komponentów rok/miesiąc/dzień w harmonogramie, żeby trafić w konkretną datę. Kiedy próbowałem, wywołania zwrotne rozszerzenia stały się zawodne, co zgadza się ze zgłoszeniami na Apple Developer Forums (wątek 729841). Trzymaj się godzin, minut i sekund.
Zastrzeżenie: te zachowania wokół północy widać tylko na urządzeniu, w nocy. Zbudowałem wokół nich zabezpieczenia, ale nie przebadałem tylu prawdziwych północy, ilu bym chciał, więc traktuj tę sekcję jako „to, co widziałem”, a nie specyfikację.
7. stopMonitoring wywołuje intervalDidEnd
zaobserwowane Wywołanie stopMonitoring, albo startMonitoring z nazwą monitora, który już działa, sprawia, że iOS wywołuje w twoim rozszerzeniu intervalDidEnd, jakby harmonogram skończył się naturalnie. Jeśli twój intervalDidEnd dla bezpieczeństwa ponownie zakłada osłonę (mój tak robi, żeby monitor nie mógł wygasnąć i zostawić aplikacji odblokowanych), to za każdym razem, gdy aplikacja zatrzymuje monitor, rozszerzenie na chwilę osłania wszystko.
Objaw zgłosił użytkownik: Spotify pauzowało za każdym razem, gdy otwierał HonorFit. Zatrzymanie monitora sprawiało, że rozszerzenie na moment zakładało osłonę na wybrane aplikacje, co wstrzymywało Spotify, zanim aplikacja z powrotem zdjęła osłony.
Dwie poprawki, obie potrzebne. Po pierwsze, usuń akcje rozszerzenia przed zatrzymaniem, żeby nie miało czego uruchomić:
function stopActivities(names: string[]): void {
for (const name of names) RNDA.cleanUpAfterActivity(name); // clear actions first
RNDA.stopMonitoring(names); // then stop
}
Po drugie, daj każdej akcji intervalDidEnd i intervalDidStart znacznik czasu „nie wcześniej niż” ustawiony na prawdziwą granicę harmonogramu, minus minuta zapasu. Rozszerzenie sprawdza go, zanim cokolwiek uruchomi:
// 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
}
W czystym Swifcie: zapisz prawdziwy czas zakończenia w App Group i niech intervalDidEnd się wycofuje, gdy zostanie wywołane za wcześnie.
8. Widmowe progi w iOS 26
Na iOS 26.x eventDidReachThreshold czasem wywołuje się bez żadnego prawdziwego użycia: zaraz po zaplanowaniu albo gdy telefon, który długo leżał zablokowany, zostanie podniesiony lub podłączony do ładowania. Apple potwierdziło to w wątkach na forum deweloperów (809410, 811305, 814559). Podobno naprawiono to w becie 26.5, ale ludzie dalej to potem zgłaszali. W mojej aplikacji objawem był „brak czasu” po tym, jak telefon poleżał w kieszeni, bo rozszerzenie ponownie zakładało osłonę, a saldo spadało do zera.
Nie powstrzymasz zdarzenia przed wywołaniem, ale możesz mu nie uwierzyć. Większość roboty robią dwa fakty o prawdziwych progach:
- Użycie nie może wyprzedzić zegara.
used-7nie może uczciwie wywołać się wcześniej niż 7 minut po uzbrojeniu monitora. Każda akcja progowa niesieneverTriggerBefore = armedAt + N minutes − 5s, więc rozszerzenie pomija niemożliwie wczesne wywołania. - Prawdziwe użycie przychodzi stopniowo, widma przychodzą seriami. Kiedy aplikacja odczytuje wywołane zdarzenia, traktuje je jako widmo, jeśli najniższy i najwyższy próg wywołały się w odstępie około 3 sekund, a dzieli je minuta lub więcej użycia.
// 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
}
Zabezpieczenie 2 działa w aplikacji, nie w rozszerzeniu, więc widmo wciąż może postawić osłonę. Saldo jednak przetrwa, a przy następnym otwarciu aplikacja z powrotem zdejmie osłonę. Wykrywanie serii w samym rozszerzeniu wymagałoby własnego kodu w Swifcie, którego jeszcze nie napisałem.
zaobserwowane Pokrewny problem działa w drugą stronę: progi, które powinny się wywołać, a się nie wywołują. Przy używaniu na zmianę (wejście, wyjście) na iOS 26 widziałem, jak pierwszy 15-sekundowy próg nowego monitora wywołuje się na czas, a późniejsze zostają scalone albo całkiem zgubione (used-0.25 się wywołał, used-0.5 i dalsze nigdy). Moje obejście: każdy monitor, który już zmierzył jakieś użycie, traktuję jako nieaktualny i za każdym razem, gdy aplikacja przechodzi w tło, uzbrajam nowy na pozostałe saldo. Daruje to do 15 sekund użycia przy każdym restarcie, z czym mogę żyć.
9. Osłona nie może otworzyć twojej aplikacji
Oczywisty pomysł to przycisk na osłonie z napisem „Zarób więcej czasu”, który otwiera twoją aplikację. Nie da się tego zrobić we wspierany sposób; Apple DTS mówi, że nie ma API, przez które rozszerzenie osłony mogłoby uruchomić swoją aplikację nadrzędną. ShieldActionDelegate odpowiada na naciśnięcie przycisku przez ShieldActionResponse (.none, .close albo .defer) i żadna z tych odpowiedzi niczego nie otwiera.
zaobserwowane Obejście, na które trafisz (NSExtensionContext().open(url) z rozszerzenia ShieldAction, które biblioteka udostępnia jako openUrl), nic nie robiło na obecnym iOS, kiedy testowałem je na urządzeniu we wrześniu 2026. Przycisk po prostu zamykał osłonę. Wysyłanie powiadomienia po stuknięciu przycisku też było u mnie zawodne.
Co wypuściłem zamiast tego: osłona ma jeden przycisk „Zamknij”, a rozszerzenie ShieldConfiguration wysyła lokalne powiadomienie za każdym razem, gdy osłona się wyświetla. Użytkownik stuka baner i ląduje w aplikacji. Aplikacja zawczasu zapisuje treść powiadomienia w App Group i usuwa powiadomienie, gdy przechodzi na pierwszy plan. iOS może prosić o konfigurację osłony kilka razy przy jednym wyświetleniu, więc wysyłanie ma 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
}
To hack i zależy od tego, czy użytkownik pozwolił na powiadomienia, ale to jedyna droga do aplikacji, która u mnie działała niezawodnie.
10. Jak zachować własne rozszerzenia przy prebuild
biblioteka Jeśli używasz Expo, expo prebuild uruchamia config plugin react-native-device-activity, który domyślnie przy każdym uruchomieniu kopiuje standardowe źródła rozszerzeń z biblioteki do ./targets. Mój dostosowany ShieldConfiguration został po cichu nadpisany. Rozwiązaniem jest jedna opcja pluginu:
["react-native-device-activity", {
"appleTeamId": "XXXXXXXXXX",
"appGroup": "group.com.honorfit.blocker",
"copyToTargetFolder": false
}]
Koszt: przy aktualizacji biblioteki musisz ręcznie porównać jej targets/ i wspólny kod Swift ze swoimi i ponownie nanieść swoje zmiany.
11. Xcode 26, Symulator i testowanie
- Buduj z SDK iOS 26. App Store Connect odrzuca teraz uploady zbudowane z SDK starszym niż iOS 26 (ITMS-90725). Na EAS przypinam obraz buildu do
macos-tahoe-26.5-xcode-26.6. - Symulator prowadzi tylko do połowy drogi. W symulatorach iOS 26 prośba o autoryzację i okno wyboru się pojawiają. Ale autoryzacja nigdy się nie kończy („Allow with Passcode” wymaga kodu urządzenia, którego w Symulatorze nie da się ustawić), okno wyboru nie ma żadnych aplikacji do pokazania, a rozszerzenie monitora nigdy się nie uruchamia. Wszystko z sekcji od 5 do 9 wymaga fizycznego urządzenia.
- Uzależnij wywołania ManagedSettings od autoryzacji. Dotknięcie store'a bez autoryzacji wyskakiwało w Symulatorze systemową prośbą o kod, co psuło mi testy UI. Przed każdym wywołaniem związanym z osłoną sprawdzam status autoryzacji.
- Pokazuj błędy autoryzacji w interfejsie. Testerzy z TestFlight nie mają konsoli. Jeśli
requestAuthorizationzawiedzie po cichu, przycisk po prostu wygląda na zepsuty. Pokazuję błąd i status w alercie. - Testuj przez noc. Moich najgorszych błędów (blokada przez całą noc mimo pozostałego czasu, pauzująca muzyka) nie dało się wyłapać w code review i odtwarzały się tylko na urządzeniu, w ciągu wielu godzin.
12. A na Androidzie
Dla porównania: Android nie ma oficjalnego odpowiednika, więc budujesz to sam. Blokada HonorFit na Androidzie to usługa pierwszoplanowa (typu specialUse, bo blokowanie aplikacji nie pasuje do żadnego z wymienionych typów), która raz na sekundę, gdy ekran jest włączony, odpytuje UsageStatsManager, żeby wiedzieć, która aplikacja jest na wierzchu. Kiedy na ekranie jest zablokowana aplikacja, a saldo wynosi zero, rysuje nad nią ekran blokady TYPE_APPLICATION_OVERLAY („Wyświetlanie nad innymi aplikacjami”). Odbiornik zdarzenia uruchomienia systemu i alarm-strażnik restartują usługę po ponownym uruchomieniu telefonu, aktualizacjach aplikacji i po agresywnych menedżerach baterii producentów.
Kompromisy idą w odwrotną stronę niż na iOS. Widzisz prawdziwe nazwy pakietów, więc prywatność to obietnica, której dotrzymujesz, a nie coś, co wymusza system. Zablokowana aplikacja może być widoczna przez maksymalnie jeden interwał odpytywania i dalej działa pod nakładką. No i będziesz pisać deklaracje w Play Console dla typu usługi pierwszoplanowej, uprawnienia do nakładki i widoczności pakietów. Celowo uniknąłem AccessibilityService, które przechodzi najsurowszą weryfikację ze wszystkich.
13. Lista kontrolna
| Objaw | Prawdopodobna przyczyna | Rozwiązanie |
|---|---|---|
| Provisioning nie działa dla jednego targetu | Brak uprawnienia albo App Group dla tego bundle ID | Złóż wniosek o Family Controls (Distribution) i podepnij App Group dla każdego bundle ID |
| Nigdy żadnych zdarzeń progowych | intervalStart = teraz, więc monitor jest zaplanowany na jutro | Cofnij początek; utrzymaj przedział ≥ 15 min |
| Monitor planowany na jutro działa już dziś | Harmonogram obejmuje bieżącą godzinę | Harmonogram, który dziś już wygasł, [00:00, T), albo repeats: true z zabezpieczonymi akcjami |
| Aplikacje na moment dostają osłonę, gdy otwierasz swoją aplikację | stopMonitoring wywołuje intervalDidEnd | Usuń akcje przed zatrzymaniem; zabezpiecz czasem „nie wcześniej niż” |
| „Brak czasu” po tym, jak telefon leżał nieużywany | Widmowe progi w iOS 26 | Zabezpieczenie zegarowe plus filtr serii |
| Późniejsze progi nigdy się nie wywołują | Gubione/scalane przy przerywanym używaniu | Uzbrajaj nowy monitor przy każdym przejściu w tło |
| Przycisk osłony nie otwiera aplikacji | Brak wspieranego API | Wysyłaj powiadomienie z ShieldConfiguration przy wyświetleniu |
| Wywołane zdarzenia nie pasują (RN) | Podkreślenie w nazwie zdarzenia | Używaj used-7, nie used_7 |
| Okno wyboru otwiera się puste (RN) | Brak propa includeEntireCategory | Przekaż go, z dowolną wartością |
14. Krótkie odpowiedzi
Czy uprawnienie Family Controls jest potrzebne dla każdego rozszerzenia?
Tak. Przyznaje się je dla każdego bundle ID osobno. Aplikacja plus rozszerzenia DeviceActivityMonitor, ShieldConfiguration i ShieldAction to cztery zatwierdzenia, a każdy App ID musi też mieć wspólną App Group.
Dlaczego eventDidReachThreshold w DeviceActivity się nie wywołuje?
U mnie zwykle chodziło o niepowtarzalny harmonogram zaczynający się „teraz”, który zostaje zaplanowany na jutro. Cofnij intervalStart. Na iOS 26, przy używaniu na zmianę (wejście, wyjście), późniejsze progi poniżej minuty też bywały gubione; pomagało uzbrojenie nowego monitora.
Czy rozszerzenie ShieldAction albo ShieldConfiguration może otworzyć aplikację nadrzędną?
Nie przez wspierane API, a obejście z NSExtensionContext w moich testach nic nie robiło na obecnym iOS. Wypuściłem lokalne powiadomienie wysyłane z ShieldConfiguration, gdy osłona się wyświetla.
Czy da się testować Screen Time API w Symulatorze iOS?
Częściowo. W symulatorach iOS 26 pojawia się prośba o autoryzację i okno wyboru, ale autoryzacja się nie kończyła, okno wyboru nie miało nic do pokazania, a rozszerzenie monitora nigdy się nie uruchomiło. Używaj urządzenia.
15. Po co mi to wszystko
Zbudowałem HonorFit, blokadę aplikacji, w której zarabiasz czas przed ekranem, robiąc pompki. Działa na słowo honoru: wpisujesz swoje powtórzenia i nie ma żadnej kamery, AI ani połączeń sieciowych. Wszystko powyżej to cena tego, żeby na iPhonie zadziałało „zarobione minuty schodzą tylko wtedy, gdy zablokowana aplikacja jest na ekranie”. Jeśli chcesz zobaczyć to w działaniu, aplikacja jest w App Store i Google Play. Jeśli trafiłeś tu, bo chcesz takiej aplikacji używać, a nie ją budować, opisałem, jak działają blokady aplikacji za pompki (po angielsku), i przygotowałem uczciwe zestawienie tych, które są dostępne (po angielsku), łącznie z konkurencją. Jeśli trafiłeś na zachowanie Czasu przed ekranem, którego tu nie ma, albo odkryłeś, że coś z powyższych nie zgadza się na nowszym iOS, chętnie o tym usłyszę.