<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thomas Willheim</title>
    <description>The latest articles on DEV Community by Thomas Willheim (@twillywill).</description>
    <link>https://dev.to/twillywill</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4169597%2Fb9364e67-2d4a-48c3-bbc9-b0a5f1a1b756.png</url>
      <title>DEV Community: Thomas Willheim</title>
      <link>https://dev.to/twillywill</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/twillywill"/>
    <language>en</language>
    <item>
      <title>The iOS Screen Time API in 2026: What Apple's Docs Don't Tell You</title>
      <dc:creator>Thomas Willheim</dc:creator>
      <pubDate>Wed, 07 Oct 2026 20:15:59 +0000</pubDate>
      <link>https://dev.to/twillywill/the-ios-screen-time-api-in-2026-what-apples-docs-dont-tell-you-151e</link>
      <guid>https://dev.to/twillywill/the-ios-screen-time-api-in-2026-what-apples-docs-dont-tell-you-151e</guid>
      <description>&lt;p&gt;&lt;em&gt;FamilyControls, DeviceActivity and shield extensions, from shipping an app blocker.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I spent August and September 2026 shipping an app blocker on iOS. It uses the Screen Time frameworks (FamilyControls, DeviceActivity, ManagedSettings and ManagedSettingsUI) from React Native, through Expo SDK 57 and the &lt;a href="https://github.com/kingstinct/react-native-device-activity" rel="noopener noreferrer"&gt;react-native-device-activity&lt;/a&gt; library (0.6.1). Apple's documentation tells you what each type is. It says very little about how they behave on a real phone, overnight, on iOS 26.&lt;/p&gt;

&lt;p&gt;This is the list I wish I'd had. Behavior I saw on a device but can't find documented is labeled &lt;strong&gt;[observed]&lt;/strong&gt;; it may not hold on every iOS version. Items specific to the React Native library are labeled &lt;strong&gt;[library]&lt;/strong&gt;; skip those if you write Swift directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The moving parts&lt;/li&gt;
&lt;li&gt;Entitlements: one per bundle ID&lt;/li&gt;
&lt;li&gt;The App Group is the only channel&lt;/li&gt;
&lt;li&gt;Tokens are opaque, and the picker&lt;/li&gt;
&lt;li&gt;Metering usage with threshold events&lt;/li&gt;
&lt;li&gt;Schedules don't start when you think&lt;/li&gt;
&lt;li&gt;stopMonitoring fires intervalDidEnd&lt;/li&gt;
&lt;li&gt;iOS 26 phantom thresholds&lt;/li&gt;
&lt;li&gt;A shield can't open your app&lt;/li&gt;
&lt;li&gt;Keeping custom extensions through prebuild&lt;/li&gt;
&lt;li&gt;Xcode 26, the Simulator, and testing&lt;/li&gt;
&lt;li&gt;And on Android&lt;/li&gt;
&lt;li&gt;Checklist&lt;/li&gt;
&lt;li&gt;Quick answers&lt;/li&gt;
&lt;li&gt;Why I needed all this&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. The moving parts
&lt;/h2&gt;

&lt;p&gt;"The Screen Time API" is four frameworks and, in practice, three app extensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FamilyControls&lt;/strong&gt;: authorization (&lt;code&gt;AuthorizationCenter&lt;/code&gt;, with &lt;code&gt;.individual&lt;/code&gt; for a self-control app) and the &lt;code&gt;FamilyActivityPicker&lt;/code&gt;, which returns a &lt;code&gt;FamilyActivitySelection&lt;/code&gt; of app, category and web-domain tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ManagedSettings&lt;/strong&gt;: a &lt;code&gt;ManagedSettingsStore&lt;/code&gt; you put those tokens into to shield them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeviceActivity&lt;/strong&gt;: schedules and usage-threshold events. The callbacks (&lt;code&gt;intervalDidStart&lt;/code&gt;, &lt;code&gt;intervalDidEnd&lt;/code&gt;, &lt;code&gt;eventDidReachThreshold&lt;/code&gt;, and the warning variants) are delivered to a &lt;code&gt;DeviceActivityMonitor&lt;/code&gt; extension, not to your app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ManagedSettingsUI&lt;/strong&gt;: a &lt;code&gt;ShieldConfigurationDataSource&lt;/code&gt; extension that styles the shield, and a &lt;code&gt;ShieldActionDelegate&lt;/code&gt; extension that handles its buttons.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extensions are the point. My app is usually suspended or killed when a user runs out of time, so the monitor extension is what puts the shield back up. Design as if your app process will never be there when things happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Entitlements: one request per bundle ID
&lt;/h2&gt;

&lt;p&gt;The Family Controls (Distribution) entitlement is approved &lt;strong&gt;per bundle ID&lt;/strong&gt;. With three extensions, that's four requests. Mine were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;com.honorfit.blocker&lt;/code&gt; (the app)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;com.honorfit.blocker.ShieldConfiguration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;com.honorfit.blocker.ShieldAction&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;com.honorfit.blocker.ActivityMonitorExtension&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once they're approved, enable the Family Controls (Distribution) capability on &lt;em&gt;each&lt;/em&gt; App ID in the developer portal, then create one App Group and attach it to all four. Miss one and that target won't provision. Approvals aren't instant, so file all four requests on the first day, before you've written much code.&lt;/p&gt;

&lt;p&gt;Every extension's entitlements file ends up the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.developer.family-controls&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.application-groups&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;group.com.honorfit.blocker&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Expo/EAS, the build service only creates provisioning profiles for extensions it knows about. I declare each target, with its entitlements, under &lt;code&gt;extra.eas.build.experimental.ios.appExtensions&lt;/code&gt; in &lt;code&gt;app.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"appExtensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bundleIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.honorfit.blocker.ShieldConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"targetName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ShieldConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"entitlements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"com.apple.developer.family-controls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"com.apple.security.application-groups"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"group.com.honorfit.blocker"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...same&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ShieldAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ActivityMonitorExtension&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. The App Group is the only channel
&lt;/h2&gt;

&lt;p&gt;Your app and its extensions are separate processes that are almost never running at the same time. The one thing they share is the App Group container, usually as &lt;code&gt;UserDefaults(suiteName:)&lt;/code&gt;. Everything the extensions need (the stored selection, the shield text, what to do when a threshold fires) has to be written there ahead of time.&lt;/p&gt;

&lt;p&gt;react-native-device-activity takes this all the way. The app writes a list of "actions" under a key like &lt;code&gt;actions_for_{activity}_{callback}_{event}&lt;/code&gt;, and the monitor extension looks them up &lt;em&gt;when the callback fires&lt;/em&gt;. A useful consequence: you can change what an already-running monitor does without restarting it. The extensions also call &lt;code&gt;CFPreferencesAppSynchronize&lt;/code&gt; before reading, so they see the app's latest writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;appGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;forInfoDictionaryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"REACT_NATIVE_DEVICE_ACTIVITY_APP_GROUP"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;userDefaults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;suiteName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;appGroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// in each callback:&lt;/span&gt;
&lt;span class="kt"&gt;CFPreferencesAppSynchronize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kCFPreferencesCurrentApplication&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userDefaults&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;triggeredBy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* run them */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Tokens are opaque, and the picker
&lt;/h2&gt;

&lt;p&gt;The picker hands your app &lt;code&gt;ApplicationToken&lt;/code&gt;s, &lt;code&gt;ActivityCategoryToken&lt;/code&gt;s and &lt;code&gt;WebDomainToken&lt;/code&gt;s, and nothing else. By design, the app can't turn a token back into "Instagram". What it can do is count them, so my blocklist screen shows "3 apps, 1 category". That's the whole privacy model, and it's a good one: my app literally doesn't know what you blocked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgi9pzu54jfle50m3bji.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgi9pzu54jfle50m3bji.webp" alt="The iOS Screen Time app picker: a list of app categories, with Health &amp;amp; Fitness expanded to show its apps" width="660" height="1310"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Apple's &lt;code&gt;FamilyActivityPicker&lt;/code&gt;. The user sees app names and icons; your app only gets opaque tokens back.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The shield extension is different. There, the system passes you an &lt;code&gt;Application&lt;/code&gt; for the app being shielded, and I use its &lt;code&gt;localizedDisplayName&lt;/code&gt; in the shield text. The app itself only ever sees tokens.&lt;/p&gt;

&lt;p&gt;Two practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Categories stay categories.&lt;/strong&gt; If the user picks "Social", you get a category token, not the apps inside it, unless the selection was created with &lt;code&gt;includeEntireCategory&lt;/code&gt;. I keep it &lt;code&gt;false&lt;/code&gt; so the counts I show are honest ("1 category", not a count of apps I can't name).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[library]&lt;/strong&gt; react-native-device-activity's persisted picker only preloads the saved selection (the checkmarks on reopen) if you pass the &lt;code&gt;includeEntireCategory&lt;/code&gt; prop, with &lt;em&gt;any&lt;/em&gt; value. Its native setter is what triggers the load. Without the prop, the picker opens empty every time even though the selection was saved.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DeviceActivitySelectionSheetViewPersisted&lt;/span&gt;
  &lt;span class="na"&gt;familyActivitySelectionId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"distracting-apps"&lt;/span&gt;
  &lt;span class="na"&gt;includeEntireCategory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// must be present, or nothing preloads&lt;/span&gt;
  &lt;span class="na"&gt;onSelectionChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onDismissRequest&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setPickerVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;[library]&lt;/strong&gt; One more: the library's &lt;code&gt;blockSelection&lt;/code&gt; &lt;em&gt;adds&lt;/em&gt; tokens to a persisted blocklist. After the user edits their selection, the removed apps stay shielded. I call &lt;code&gt;resetBlocks()&lt;/code&gt; first and then re-block the current selection every time.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Metering usage with threshold events
&lt;/h2&gt;

&lt;p&gt;HonorFit gives you a balance of minutes that only drains while a blocked app is on screen. DeviceActivity has no "tell me how long they've used it" call; it has threshold events that fire once cumulative usage of a selection reaches a duration. So I register one event per 15 seconds of usage, named in minutes: &lt;code&gt;used-0.25&lt;/code&gt;, &lt;code&gt;used-0.5&lt;/code&gt;, … up to the balance. Quarters are exact in binary floating point, so the names round-trip through &lt;code&gt;Number()&lt;/code&gt;. Under the library, this is the plain Apple API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;schedule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;DeviceActivitySchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;intervalStart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;DateComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;// see section 6&lt;/span&gt;
  &lt;span class="nv"&gt;intervalEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;DateComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nv"&gt;repeats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;DeviceActivityEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;applications&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;applicationTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;categoryTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;webDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;webDomainTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;DateComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nv"&gt;includesPastActivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;          &lt;span class="c1"&gt;// iOS 17.4+&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;DeviceActivityCenter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startMonitoring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kt"&gt;DeviceActivityName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"honorfit-distracting-apps-1790000000000"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nv"&gt;during&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;DeviceActivityEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"used-0.25"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;// ...one per step&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;includesPastActivity: false&lt;/code&gt; (the default) means usage from before the events were registered doesn't count. That matters for the schedule trick in the next section.&lt;/p&gt;

&lt;p&gt;When the final event fires, the monitor extension re-applies the shield and posts a "time's up" notification. Each 15-second event updates one notification with a fixed identifier, so it replaces in place and works as a rough live countdown. &lt;code&gt;interruptionLevel = .passive&lt;/code&gt; delivers it quietly to Notification Center with no banner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[library]&lt;/strong&gt; &lt;strong&gt;Don't put underscores in event or activity names.&lt;/strong&gt; The library persists fired events under &lt;code&gt;events_{activity}_{callback}_{eventName}&lt;/code&gt; and parses them back by splitting on &lt;code&gt;_&lt;/code&gt;. An event called &lt;code&gt;used_7&lt;/code&gt; comes back as &lt;code&gt;used&lt;/code&gt;, and nothing ever matches. The symptom was a balance that never went down.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Schedules don't start when you think
&lt;/h2&gt;

&lt;p&gt;If your threshold events silently never fire, it's probably this.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Now" is already in the past
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;[observed]&lt;/strong&gt; A non-repeating &lt;code&gt;DeviceActivitySchedule&lt;/code&gt; is matched against date components, and the monitor runs the next interval that matches. If you pass the current wall-clock time as &lt;code&gt;intervalStart&lt;/code&gt;, then by the time the framework evaluates it, that moment has passed. The next match is &lt;strong&gt;tomorrow&lt;/strong&gt;, and today nothing fires: no thresholds, no callbacks, no error. The fix is to backdate the start so "now" is inside the interval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backdated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intervalStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;backdated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;backdated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;backdated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMinutes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;backdated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSeconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;   &lt;span class="c1"&gt;// don't wrap past midnight&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;startMonitoring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activityName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;intervalStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;intervalEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;59&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;repeats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Backdating can't charge the user for earlier scrolling, because of &lt;code&gt;includesPastActivity: false&lt;/code&gt;. It also keeps the interval above Apple's minimum: schedules shorter than 15 minutes are rejected, which bites anyone starting a session just before midnight.&lt;/p&gt;

&lt;h3&gt;
  
  
  A schedule that contains "now" starts immediately
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;[observed]&lt;/strong&gt; The flip side: a 00:00–23:59:59 schedule contains the current time, so it starts &lt;em&gt;now&lt;/em&gt;, not at the next midnight. Two ways I found to book tomorrow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An already-expired schedule.&lt;/strong&gt; &lt;code&gt;[00:00, T)&lt;/code&gt; where T is a minute before the current time of day. Today's instance is over, so the framework books tomorrow's. I use this to carry an unspent balance past midnight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;repeats: true&lt;/code&gt; with guarded actions.&lt;/strong&gt; Today's instance starts immediately and does nothing (every action is guarded, see the next section). Tomorrow's does the real work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;[observed]&lt;/strong&gt; Don't use full year/month/day components in a schedule to target a specific date. Extension callbacks became unreliable when I tried, which matches reports on the Apple Developer Forums (&lt;a href="https://developer.apple.com/forums/thread/729841" rel="noopener noreferrer"&gt;thread 729841&lt;/a&gt;). Stick to hour/minute/second.&lt;/p&gt;

&lt;p&gt;A caveat: these midnight behaviors only show up on a device, overnight. I've built guards around them but haven't instrumented as many real midnights as I'd like, so treat this section as "what I saw", not a spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. stopMonitoring fires intervalDidEnd
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;[observed]&lt;/strong&gt; Calling &lt;code&gt;stopMonitoring&lt;/code&gt;, or calling &lt;code&gt;startMonitoring&lt;/code&gt; with the name of a monitor that's already running, makes iOS call &lt;code&gt;intervalDidEnd&lt;/code&gt; on your extension as if the schedule had ended naturally. If your &lt;code&gt;intervalDidEnd&lt;/code&gt; re-shields as a safety net (mine does, so a monitor can't expire and leave apps unblocked), then every time the app stops a monitor, the extension briefly shields everything.&lt;/p&gt;

&lt;p&gt;A user reported the symptom: Spotify paused every time they opened HonorFit. Stopping the monitor made the extension shield the selection for a moment, which suspended Spotify, before the app lifted the shields again.&lt;/p&gt;

&lt;p&gt;Two fixes, both needed. First, delete the extension's actions &lt;em&gt;before&lt;/em&gt; stopping, so there's nothing for it to run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stopActivities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;RNDA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cleanUpAfterActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// clear actions first&lt;/span&gt;
  &lt;span class="nx"&gt;RNDA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopMonitoring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                                &lt;span class="c1"&gt;// then stop&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, give every &lt;code&gt;intervalDidEnd&lt;/code&gt; and &lt;code&gt;intervalDidStart&lt;/code&gt; action a "not before" timestamp at the schedule's real boundary, minus a minute of slack. The extension checks it before running anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TypeScript: register the safety net&lt;/span&gt;
&lt;span class="nx"&gt;RNDA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configureActions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;activityName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;callbackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;intervalDidEnd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blockSelection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;familyActivitySelectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;distracting-apps&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;neverTriggerBefore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intervalEndMs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Swift, in the DeviceActivityMonitor extension&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;neverTriggerBefore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;neverTriggerBefore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeIntervalSince1970&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;neverTriggerBefore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// skip this action&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain Swift: store the genuine end time in the App Group and have &lt;code&gt;intervalDidEnd&lt;/code&gt; bail out when called early.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. iOS 26 phantom thresholds
&lt;/h2&gt;

&lt;p&gt;On iOS 26.x, &lt;code&gt;eventDidReachThreshold&lt;/code&gt; sometimes fires with no real usage at all: right after scheduling, or when a phone that sat locked for a while is picked up or plugged in. Apple has acknowledged this in developer forum threads (&lt;a href="https://developer.apple.com/forums/thread/809410" rel="noopener noreferrer"&gt;809410&lt;/a&gt;, &lt;a href="https://developer.apple.com/forums/thread/811305" rel="noopener noreferrer"&gt;811305&lt;/a&gt;, &lt;a href="https://developer.apple.com/forums/thread/814559" rel="noopener noreferrer"&gt;814559&lt;/a&gt;). It was reportedly fixed in a 26.5 beta, but people kept reporting it afterwards. In my app the symptom was "out of time" after the phone had been in a pocket, because the extension re-shielded and the balance drained to zero.&lt;/p&gt;

&lt;p&gt;You can't stop the event from firing, but you can refuse to believe it. Two facts about genuine thresholds do most of the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Usage can't outrun the wall clock.&lt;/strong&gt; &lt;code&gt;used-7&lt;/code&gt; can't legitimately fire less than 7 minutes after the monitor was armed. Every threshold action carries &lt;code&gt;neverTriggerBefore = armedAt + N minutes − 5s&lt;/code&gt;, so the extension skips impossible-early fires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real usage arrives incrementally; phantoms arrive as a burst.&lt;/strong&gt; When the app reads back fired events, it treats it as a phantom when the lowest and highest thresholds fired within about 3 seconds of each other but span a minute or more of usage.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Guard 1: drop thresholds that fired before they could have been reached&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Guard 2: lowest and highest fired in the same instant, a minute or more apart&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;highest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lowest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;highest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lowest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;usedMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;partialMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;   &lt;span class="c1"&gt;// phantom: ignore the lot&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guard 2 runs in the app, not the extension, so a phantom can still put the shield up. The balance survives, though, and the next time the app is opened it lifts the shield again. Doing burst detection inside the extension would need custom Swift, which I haven't written yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[observed]&lt;/strong&gt; A related problem goes the other way: thresholds that should fire and don't. With in-and-out usage on iOS 26, I saw a fresh monitor's first 15-second threshold fire on time, and later ones get coalesced or dropped entirely (&lt;code&gt;used-0.25&lt;/code&gt; fired, &lt;code&gt;used-0.5&lt;/code&gt; and up never did). My workaround is to treat any monitor that has already metered usage as stale and re-arm a fresh one, over the remaining balance, every time the app goes to the background. That forgives up to 15 seconds of usage per restart, which I'm fine with.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. A shield can't open your app
&lt;/h2&gt;

&lt;p&gt;The obvious design is a shield button that says "Earn more time" and opens your app. There's no supported way to do it; Apple DTS says there's no API for a shield extension to launch its parent app. A &lt;code&gt;ShieldActionDelegate&lt;/code&gt; answers a button press with a &lt;code&gt;ShieldActionResponse&lt;/code&gt; (&lt;code&gt;.none&lt;/code&gt;, &lt;code&gt;.close&lt;/code&gt; or &lt;code&gt;.defer&lt;/code&gt;), and none of those opens anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[observed]&lt;/strong&gt; The workaround you'll find (&lt;code&gt;NSExtensionContext().open(url)&lt;/code&gt; from the ShieldAction extension, which the library exposes as &lt;code&gt;openUrl&lt;/code&gt;) did nothing on current iOS when I tested it on a device in September 2026. The button just closed the shield. Posting a notification from the button tap was unreliable for me too.&lt;/p&gt;

&lt;p&gt;What I ship instead: the shield has a single "Close" button, and the &lt;strong&gt;ShieldConfiguration&lt;/strong&gt; extension posts a local notification whenever the shield is &lt;em&gt;displayed&lt;/em&gt;. The user taps the banner and lands in the app. The app writes the notification payload to the App Group ahead of time, and dismisses the notification when it comes to the foreground. iOS may ask for the shield configuration several times per display, so the post is debounced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;postShieldShownNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;placeholders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userDefaults&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dictionary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"honorfitShieldShownNotification"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;minInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"minIntervalSeconds"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeIntervalSince1970&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;lastSentAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userDefaults&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"honorfitShieldShownNotificationLastSentAt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lastSentAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;minInterval&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;userDefaults&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"honorfitShieldShownNotificationLastSentAt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// builds a UNMutableNotificationContent and adds a UNNotificationRequest&lt;/span&gt;
  &lt;span class="c1"&gt;// with a fixed identifier, so repeats replace rather than stack&lt;/span&gt;
  &lt;span class="nf"&gt;sendNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;placeholders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;placeholders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;ShieldConfigurationExtension&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;ShieldConfigurationDataSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shielding&lt;/span&gt; &lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;ShieldConfiguration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;postShieldShownNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;placeholders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s"&gt;"applicationOrDomainDisplayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDisplayName&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;buildShield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* title, subtitle, colors from the App Group */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...same hook in the category and web-domain overloads&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a hack, and it depends on the user allowing notifications, but it's the only route into the app that worked reliably for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Keeping custom extensions through prebuild
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;[library]&lt;/strong&gt; If you use Expo, &lt;code&gt;expo prebuild&lt;/code&gt; runs the react-native-device-activity config plugin, which by default copies the library's stock extension sources into &lt;code&gt;./targets&lt;/code&gt; on every run. My customized ShieldConfiguration was silently replaced. The fix is one plugin option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"react-native-device-activity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appleTeamId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"XXXXXXXXXX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"group.com.honorfit.blocker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"copyToTargetFolder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost: when you upgrade the library, diff its &lt;code&gt;targets/&lt;/code&gt; and shared Swift against yours by hand and re-apply your changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Xcode 26, the Simulator, and testing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build with the iOS 26 SDK.&lt;/strong&gt; App Store Connect now rejects uploads built with an SDK older than iOS 26 (ITMS-90725). On EAS I pin the build image to &lt;code&gt;macos-tahoe-26.5-xcode-26.6&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Simulator only gets you partway.&lt;/strong&gt; On iOS 26 simulators, the authorization prompt and the picker do appear. But authorization never completes ("Allow with Passcode" wants a device passcode the Simulator can't set), the picker has no apps to list, and the monitor extension never runs. Everything in sections 5 to 9 needs a physical device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard ManagedSettings calls on authorization.&lt;/strong&gt; Touching the store while unauthorized popped a system passcode prompt on the Simulator, which broke my UI tests. I check authorization status before any shield call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surface authorization errors in the UI.&lt;/strong&gt; TestFlight testers don't have a console. If &lt;code&gt;requestAuthorization&lt;/code&gt; fails silently, the button just looks broken. I show the error and status in an alert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test overnight.&lt;/strong&gt; My worst bugs (blocked all night with time left, music pausing) were invisible in code review and only reproduced on a device over hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. And on Android
&lt;/h2&gt;

&lt;p&gt;For contrast, Android has no sanctioned equivalent, so you build it yourself. HonorFit's Android blocker is a foreground service (type &lt;code&gt;specialUse&lt;/code&gt;, since app blocking fits none of the listed types) that polls &lt;code&gt;UsageStatsManager&lt;/code&gt; once a second while the screen is on to learn which app is in front. When a blocked app is up and the balance is zero, it draws a &lt;code&gt;TYPE_APPLICATION_OVERLAY&lt;/code&gt; lock screen over it ("Display over other apps"). A boot receiver and a watchdog alarm restart the service after reboots, app updates, and aggressive OEM battery managers.&lt;/p&gt;

&lt;p&gt;The trade-offs point the opposite way from iOS. You see real package names, so privacy is a promise you keep rather than one the OS enforces. A blocked app can be visible for up to one poll interval, and it keeps running under the overlay. And you'll write Play Console declarations for the foreground service type, the overlay permission and package visibility. I deliberately avoided an AccessibilityService, which gets the strictest review of all.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Provisioning fails for one target&lt;/td&gt;
&lt;td&gt;Entitlement or App Group missing on that bundle ID&lt;/td&gt;
&lt;td&gt;Request Family Controls (Distribution) and attach the App Group for every bundle ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No threshold events, ever&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;intervalStart&lt;/code&gt; = now, so the monitor is booked for tomorrow&lt;/td&gt;
&lt;td&gt;Backdate the start; keep the interval ≥ 15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitor meant for tomorrow runs today&lt;/td&gt;
&lt;td&gt;Schedule contains the current time&lt;/td&gt;
&lt;td&gt;Expired-today &lt;code&gt;[00:00, T)&lt;/code&gt; schedule, or &lt;code&gt;repeats: true&lt;/code&gt; with guarded actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apps flash-shield when your app opens&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;stopMonitoring&lt;/code&gt; fires &lt;code&gt;intervalDidEnd&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Clear actions before stopping; guard with a not-before time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Out of time" after the phone sat idle&lt;/td&gt;
&lt;td&gt;iOS 26 phantom thresholds&lt;/td&gt;
&lt;td&gt;Wall-clock guard plus burst filter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Later thresholds never fire&lt;/td&gt;
&lt;td&gt;Dropped/coalesced under intermittent use&lt;/td&gt;
&lt;td&gt;Re-arm a fresh monitor on each background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shield button won't open the app&lt;/td&gt;
&lt;td&gt;No supported API&lt;/td&gt;
&lt;td&gt;Post a notification from ShieldConfiguration on display&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fired events don't match (RN)&lt;/td&gt;
&lt;td&gt;Underscore in event name&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;used-7&lt;/code&gt;, not &lt;code&gt;used_7&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Picker opens empty (RN)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;includeEntireCategory&lt;/code&gt; prop missing&lt;/td&gt;
&lt;td&gt;Pass it, with any value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  14. Quick answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need the Family Controls entitlement for every extension?&lt;/strong&gt;&lt;br&gt;
Yes. It's granted per bundle ID. The app plus a DeviceActivityMonitor, a ShieldConfiguration and a ShieldAction extension is four approvals, and each App ID also needs the shared App Group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is DeviceActivity eventDidReachThreshold not firing?&lt;/strong&gt;&lt;br&gt;
For me it was usually a non-repeating schedule starting at "now", which gets booked for tomorrow. Backdate &lt;code&gt;intervalStart&lt;/code&gt;. On iOS 26, under in-and-out usage, later sub-minute thresholds also got dropped; re-arming a fresh monitor helped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a ShieldAction or ShieldConfiguration extension open the parent app?&lt;/strong&gt;&lt;br&gt;
Not with a supported API, and the &lt;code&gt;NSExtensionContext&lt;/code&gt; workaround did nothing on current iOS in my testing. A local notification posted from ShieldConfiguration when the shield is displayed is what I ship.&lt;br&gt;
The iOS Screen Time API in 2026: What Apple's Docs Don't Tell You&lt;br&gt;
&lt;strong&gt;Can I test the Screen Time API in the iOS Simulator?&lt;/strong&gt;&lt;br&gt;
Partly. The prompt and picker appear on iOS 26 simulators, but authorization didn't complete, the picker had nothing to list, and the monitor extension never ran. Use a device.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Why I needed all this
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://honorfitapp.com/" rel="noopener noreferrer"&gt;HonorFit&lt;/a&gt;, an app blocker where you earn screen time by doing pushups. It works on the honor system: you type in your reps, and there's no camera, no AI and no network calls. Everything above is what it took to make "your earned minutes only drain while a blocked app is on screen" work on an iPhone. If you want to see it running, it's &lt;a href="https://honorfitapp.com/get/?c=devto" rel="noopener noreferrer"&gt;on the App Store and Google Play&lt;/a&gt;. If you've hit a Screen Time behavior that isn't here, or found that something above is wrong on a newer iOS, I'd like to hear about it.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>reactnative</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
