logo hsb.horse
← Back to blog index

Blog

An iOS Shield UI Needs Two Extensions, Not One

The shield UI shown by ManagedSettings is made up of two separate targets: an Action Extension and a Configuration Extension. This article organizes the correct NSExtensionPointIdentifier values and principal classes.

Published:

When you configure blocking rules with ManagedSettings, iOS shows a shield screen when the user accesses a blocked site. There is one trap that almost everyone hits the first time they try to customize this shield UI: one extension target is not enough. You need two.

An iOS extension is a small process that runs separately from the main app. In Xcode, you add it as another target. A shield extension is a dedicated extension for the shield UI and runs independently from the main app.

Shield Action Extension

This handles what happens when the user taps a button on the shield screen. The principal class, meaning the entry point for the extension, inherits from ShieldActionDelegate.

import ManagedSettings
class ShieldActionExtension: ShieldActionDelegate {
override func handle(
action: ShieldAction,
for application: ApplicationToken,
completionHandler: @escaping (ShieldActionResponse) -> Void
) {
completionHandler(.close)
}
}

The Extension Point Identifier is com.apple.ManagedSettings.shield-action-service.

This extension imports ManagedSettings, not ManagedSettingsUI. That mix-up is easy to make, so it is worth calling out explicitly.

Shield Configuration Extension

This customizes the visuals of the shield screen. Use it when you want the title, message, and button labels to match your app’s context. The principal class inherits from ShieldConfigurationDataSource.

import ManagedSettingsUI
import ManagedSettings
class ShieldConfigurationExtension: ShieldConfigurationDataSource {
override func configuration(
shielding application: Application
) -> ShieldConfiguration {
return ShieldConfiguration(
backgroundBlurStyle: .systemUltraThinMaterial,
title: ShieldConfiguration.Label(text: "集中モード中です", color: .label)
)
}
}

The Extension Point Identifier is com.apple.ManagedSettingsUI.shield-configuration-service. This one imports ManagedSettingsUI.

Info.plist settings

Set the correct Extension Point Identifier in each target’s Info.plist.

For the Action Extension:

<key>NSExtensionPointIdentifier</key>
<string>com.apple.ManagedSettings.shield-action-service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShieldActionExtension</string>

For the Configuration Extension:

<key>NSExtensionPointIdentifier</key>
<string>com.apple.ManagedSettingsUI.shield-configuration-service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShieldConfigurationExtension</string>

Do not forget App Group setup

The two extensions and the main app must share the same App Group. App Groups are the mechanism that lets multiple targets share data. On iOS, apps normally cannot exchange data directly, but targets in the same App Group can read and write from the shared container.

In Xcode, add App Groups under Signing & Capabilities for each target and set the same group ID. You need it on all three targets: the main app, the Action Extension, and the Configuration Extension. If you are new to iOS development, it is easy to think you configured it and still miss one target. If even one is missing, shared data will not work on a real device.

What you can do from the shield screen

ShieldActionDelegate can return only two values: .close and .defer. .close dismisses the shield while keeping the block in place. .defer is used to trigger additional user interaction.

You cannot directly launch the main app from the shield and show an unlock screen through this interface. That is another part that feels surprisingly restrictive when you are new to iOS development. I split that implementation pattern into a separate article.