Screen Time shield not hiding after “Access App” custom button on Shield on TestFlight (works in local debug)

Hi,

I am building an iOS app that uses FamilyControls / ManagedSettings to restrict apps.

Flow of my app:

  1. In my main app, the user chooses which apps to restrict using FamilyActivityPicker (for example, they select Instagram).
  2. I save the selection in an App Group.
  3. I then use ManagedSettingsStore in the main app to add those app tokens into store.shield.applications, so a Screen Time shield appears when the user opens Instagram.
  4. In my ShieldConfigurationExtension, I show a shield UI with a primary button called “Access App”.
  5. In my ShieldActionExtension, when the user taps “Access App”, I want to immediately hide the shield and allow Instagram.

To hide the shield, I am using this code in my ShieldActionExtension:

final class ShieldActionExtension: ShieldActionDelegate {
    // ...

    override func handle(
        action: ShieldAction,
        for application: ApplicationToken,
        completionHandler: @escaping (ShieldActionResponse) -> Void
    ) {
        switch action {
        case .primaryButtonPressed:
            handlePrimaryButton(for: application, completionHandler: completionHandler)
        case .secondaryButtonPressed:
            completionHandler(.close)
        @unknown default:
            completionHandler(.defer)
        }
    }

    private func handlePrimaryButton(
        for application: ApplicationToken,
        completionHandler: @escaping (ShieldActionResponse) -> Void
    ) {
        // (I update some app-group state here, lives, history, etc.)

        // This is the important part: I try to unshield the app
        let store = ManagedSettingsStore()
        var apps = store.shield.applications ?? Set<ApplicationToken>()
        apps.remove(application)
        store.shield.applications = apps

        // I then tell the system to re-evaluate
        completionHandler(.defer)
    }
}

(When testing another approach, I also tried completionHandler(.close) after removing the app from the shield applications.)

Behavior I see:

  • Local / Xcode debug build (installed by cable):

    • Open Instagram → Slofy shield appears.
    • Tap “Access App” → the above code runs.
    • Shield disappears immediately and Instagram is usable. ✅
  • TestFlight build:

    • Open Instagram → Slofy shield appears.
    • Tap “Access App” → the above code runs, and I see in logs: Removed app from shield set (apps now: 0)
    • But the shield does not hide. It stays on the screen. ❌
    • Only if I then open my main app (Slofy) and close it again, and then return to Instagram, the shield disappears and Instagram is unlocked.

So the same code works as expected in local debug builds, but in TestFlight builds the Screen Time shield does not refresh / disappear immediately after I remove the app from store.shield.applications inside the ShieldActionExtension.

My questions:

  1. Is it supported to unshield an app directly from inside a ShieldActionExtension (by removing it from ManagedSettingsStore().shield.applications) and expect the shield to disappear immediately?
  2. Is there any difference in how ManagedSettingsStore changes are applied between debug and TestFlight / release builds for Screen Time shields?
  3. Is the main app required to be in the foreground for the shield to update, or is there a recommended pattern to make the shield hide right after the user taps the primary button in the shield?

I would like the behavior to be:

User opens restricted app → shield shows → taps “Access App” → shield hides immediately and the app becomes usable, without needing to open the main app.

Any guidance on the correct way to implement this with Screen Time extensions would be greatly appreciated.

Thank you.

Thank you for your post. This is quite interesting for me, as I am still learning about this API myself. It is possible to “unshield” an application by modifying the ManagedSettingsStore().shield.applications array https://developer.apple.com/documentation/ManagedSettings/ShieldActionDelegate. However, the immediate disappearance of the shield depends on how the associated extension handles state updates.

Typically, invoking completionHandler(.none) after modifying settings signals the system to apply changes, but immediate UI updates may require explicit app/extension design support for dynamic reloading. If this does not work, we should examine the code.

Debug builds typically allow real-time data synchronization and immediate reflection of ManagedSettingsStore changes. However, TestFlight should have the same experience. Some functionality may not operate configurations in TestFlight builds, and you should file a bug for those. The shield does not necessarily require the main app to remain in the foreground for updates if implemented correctly. Using a .defer action in the ShieldActionExtension triggers the configuration extension to refresh the shield’s state asynchronously. A recommended pattern includes immediately returning .defer in the primary action handler while performing state updates in the background through the configuration extension.

Do you get the same results with just the relevant code in a small test project? If so, please share a link to your test project. That'll help us better understand what's going on. If you're not familiar with preparing a test project, take a look at Creating a test project.

I would recommend to file a bug for the TestFlight behavior.

Once you open the bug report, please post the FB number here for my reference.

If you have any questions about filing a bug report, take a look at Bug Reporting: How and Why?

Albert Pascual
  Worldwide Developer Relations.

Screen Time shield not hiding after “Access App” custom button on Shield on TestFlight (works in local debug)
 
 
Q