As an indie developer building a focus-time app in Rork Max, I kept hitting the same wall: the block would fire on schedule, but what appeared was Apple's default gray screen — nothing that told the user it was still my app. After a carefully made onboarding flow, users were suddenly dropped onto a cold system screen.
That "shield" screen can be replaced. But you can't touch it from your main app code. The screen shown during a block is drawn by a separate process, so reshaping it means adding two dedicated App Extensions: a ShieldConfiguration extension that swaps the appearance, and a ShieldAction extension that decides what happens when its buttons are pressed.
I'll assume you already have the Family Controls entitlement and are applying shields through ManagedSettingsStore. This article focuses on the step after that: rebuilding the screen itself.
Why you can't draw the shield from the main app
When you hand tokens to store.shield.applications, the system slides a shield screen in the moment the target app is opened. That screen isn't drawn by your app — it's a separate process running inside the ManagedSettingsUI framework. Neither React Native nor your main Swift target can touch it directly.
The one entry point is ShieldConfigurationDataSource. Right before the system shows the shield, it asks this class, and builds the screen from the ShieldConfiguration you return. So what you control is "which parts, in which colors" — not an arbitrary SwiftUI view. Miss this and you'll get stuck trying to render an elaborate layout that the API simply won't accept.
| What you want | Possible? | How |
|---|---|---|
| Change background color / blur | Yes | backgroundColor / backgroundBlurStyle |
| Use your own icon | Yes (only images bundled in the extension) | icon as a UIImage |
| Title / subtitle text and color | Yes | title / subtitle (Label) |
| Two buttons | Yes (max two — label and color only) | primaryButtonLabel / secondaryButtonLabel |
| Render an arbitrary SwiftUI view | No | — |
| Fetch a remote image on the spot | Effectively no | Bundle it, or pass via App Group |
Step 1: Add the ShieldConfiguration extension
In Xcode, add a new "Shield Configuration" extension target. When you build through Expo like Rork Max does, re-adding the target by hand on every eas build isn't realistic, so inject the extension target, its Info.plist, and entitlements through a config plugin. I keep this in the same config plugin as my main native module so the extension never gets forgotten.
In the extension's Info.plist, set NSExtensionPointIdentifier to com.apple.ManagedSettingsUI.shield-configuration-service and NSExtensionPrincipalClass to your class name. Get this wrong and the build still succeeds, but the system never calls you — you stay on the default screen, with no error to explain why.
import ManagedSettings
import ManagedSettingsUI
import UIKit
class FocusShieldConfiguration: ShieldConfigurationDataSource {
override func configuration(shielding application: Application) -> ShieldConfiguration {
return ShieldConfiguration(
backgroundBlurStyle: .systemThinMaterialDark,
backgroundColor: UIColor(red: 0.08, green: 0.10, blue: 0.16, alpha: 1),
icon: UIImage(named: "ShieldLeaf"),
title: ShieldConfiguration.Label(
text: "This is your focus time",
color: .white
),
subtitle: ShieldConfiguration.Label(
text: "You chose to close this app during this window. Give it a few more minutes.",
color: UIColor(white: 0.75, alpha: 1)
),
primaryButtonLabel: ShieldConfiguration.Label(
text: "Back to focus",
color: .white
),
primaryButtonBackgroundColor: UIColor(red: 0.20, green: 0.55, blue: 0.90, alpha: 1),
secondaryButtonLabel: ShieldConfiguration.Label(
text: "Open for 5 minutes",
color: UIColor(white: 0.65, alpha: 1)
)
)
}
// Return this too when you block by category
override func configuration(shielding application: Application,
in category: ActivityCategory) -> ShieldConfiguration {
return configuration(shielding: application)
}
}Set secondaryButtonLabel to nil and you get a single button. Specific wording pays off. Rather than announcing "blocked," a line that reminds users "this was your own promise" changes how the screen lands. Just swapping "Access is blocked" for the line above visibly warmed up my review sentiment.