How to Build Screen Time API in SwiftUI
Import FamilyControls, request authorization via
AuthorizationCenter.shared.requestAuthorization(for:), then present
FamilyActivityPicker to select apps. Apply restrictions through
ManagedSettingsStore.
import SwiftUI
import FamilyControls
import ManagedSettings
struct ContentView: View {
@State private var isPickerPresented = false
@State private var selection = FamilyActivitySelection()
private let store = ManagedSettingsStore()
var body: some View {
Button("Choose Apps to Restrict") {
isPickerPresented = true
}
.familyActivityPicker(isPresented: $isPickerPresented,
selection: $selection)
.onChange(of: selection) { _, newValue in
store.shield.applications = newValue.applicationTokens.isEmpty
? nil : newValue.applicationTokens
}
}
}
Full implementation
The Screen Time API spans three frameworks — FamilyControls (authorization and
app selection), ManagedSettings (applying shields and restrictions), and
DeviceActivity (scheduling monitors). Because
DeviceActivityMonitor must live in a separate Extension target, the example
below focuses on the main-app layer: requesting authorization, picking apps, persisting the
selection with a custom ScreenTimeModel using @Observable, and
toggling restrictions on/off. Add the com.apple.developer.family-controls
entitlement before building — without it, every API call silently fails.
import SwiftUI
import FamilyControls
import ManagedSettings
import DeviceActivity
// MARK: - Observable model (lives in main app target)
@Observable
final class ScreenTimeModel {
var authStatus: AuthorizationStatus = .notDetermined
var selection = FamilyActivitySelection()
var isRestricting = false
private let store = ManagedSettingsStore()
private let center = AuthorizationCenter.shared
// Request authorization (.individual = self-control, .family = child account)
func requestAuthorization() async {
do {
try await center.requestAuthorization(for: .individual)
authStatus = center.authorizationStatus
} catch {
print("FamilyControls auth error: \(error)")
}
}
// Apply or lift shields based on current selection
func applyRestrictions(_ enable: Bool) {
isRestricting = enable
if enable {
store.shield.applications = selection.applicationTokens.isEmpty
? nil : selection.applicationTokens
store.shield.webDomains = selection.webDomainTokens.isEmpty
? nil : selection.webDomainTokens
} else {
store.shield.applications = nil
store.shield.webDomains = nil
}
}
// Revoke all managed settings and authorization
func reset() {
store.clearAllSettings()
isRestricting = false
selection = FamilyActivitySelection()
}
}
// MARK: - Root view
struct ScreenTimeRootView: View {
@State private var model = ScreenTimeModel()
@State private var isPickerPresented = false
var body: some View {
NavigationStack {
Form {
authorizationSection
if model.authStatus == .approved {
selectionSection
restrictionSection
}
}
.navigationTitle("Screen Time")
.task { await model.requestAuthorization() }
.familyActivityPicker(
isPresented: $isPickerPresented,
selection: $model.selection
)
}
}
// MARK: Sections
private var authorizationSection: some View {
Section("Authorization") {
HStack {
Label("Status", systemImage: "lock.shield")
Spacer()
Text(statusLabel)
.foregroundStyle(model.authStatus == .approved ? .green : .orange)
}
if model.authStatus != .approved {
Button("Request Permission") {
Task { await model.requestAuthorization() }
}
}
}
}
private var selectionSection: some View {
Section("Blocked Apps & Sites") {
Button {
isPickerPresented = true
} label: {
Label("Choose Apps / Websites", systemImage: "apps.iphone")
}
if !model.selection.applicationTokens.isEmpty {
Text("\(model.selection.applicationTokens.count) app(s) selected")
.foregroundStyle(.secondary)
}
if !model.selection.webDomainTokens.isEmpty {
Text("\(model.selection.webDomainTokens.count) domain(s) selected")
.foregroundStyle(.secondary)
}
}
}
private var restrictionSection: some View {
Section("Restrictions") {
Toggle(isOn: Binding(
get: { model.isRestricting },
set: { model.applyRestrictions($0) }
)) {
Label("Block Selected", systemImage: "hand.raised.fill")
}
.disabled(model.selection.applicationTokens.isEmpty
&& model.selection.webDomainTokens.isEmpty)
Button(role: .destructive) {
model.reset()
} label: {
Label("Reset All Settings", systemImage: "trash")
}
}
}
private var statusLabel: String {
switch model.authStatus {
case .approved: return "Approved"
case .denied: return "Denied"
default: return "Not Determined"
}
}
}
#Preview {
ScreenTimeRootView()
}
How it works
-
Authorization (
requestAuthorization(for:)) — called inside.taskon the root view so the system prompt appears once at launch..individualis for users restricting themselves; use.familywhen a parent manages a child's device via iCloud Family Sharing. The status is reflected inmodel.authStatusto hide the UI until approved. -
App selection (
FamilyActivityPicker) — attached via the.familyActivityPicker(isPresented:selection:)modifier. The picker presents a system-provided sheet (Apple's UI — you can't customise it) and writes the result intoFamilyActivitySelection, which carries opaqueapplicationTokensandwebDomainTokensthat never expose bundle IDs directly. -
Applying shields (
ManagedSettingsStore) — settingstore.shield.applicationsto a non-nil set of tokens immediately shows the Screen Time shield over those apps. Setting it back tonillifts the restriction.clearAllSettings()wipes every managed setting in one call. -
@Observablemodel — using the iOS 17 Observation framework keeps the view in sync with auth status and restriction state without manualobjectWillChangecalls.ScreenTimeModelis injected via@Stateat the root and can be passed down with@Environmentin deeper views. -
Extension target for monitoring —
DeviceActivityMonitorsubclasses must live in a DeviceActivity Monitor Extension target (File → New Target in Xcode). The main app callsDeviceActivityCenter().startMonitoring(_:during:)with aDeviceActivityScheduleto trigger callbacks in that extension at defined intervals — those callbacks are where you escalate restrictions automatically.
Variants
Scheduled monitoring with DeviceActivityCenter
import DeviceActivity
// In your main app target — starts the monitor
func startDailyMonitor() {
let schedule = DeviceActivitySchedule(
intervalStart: DateComponents(hour: 0, minute: 0),
intervalEnd: DateComponents(hour: 23, minute: 59),
repeats: true
)
let center = DeviceActivityCenter()
do {
// "DailyActivity" must match the name used in your extension
try center.startMonitoring(.init("DailyActivity"), during: schedule)
} catch {
print("Monitor start failed: \(error)")
}
}
// In the DeviceActivity Monitor Extension target:
// class MyMonitor: DeviceActivityMonitor {
// override func intervalDidStart(for activity: DeviceActivityName) { ... }
// override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name,
// activity: DeviceActivityName) { ... }
// }
Custom shield appearance with ShieldConfigurationDataSource
Add a Shield Configuration Extension target and subclass
ShieldConfigurationDataSource. Override
configuration(shielding:) to return a custom ShieldConfiguration
with your own title, subtitle, icon, and background colour. This runs out-of-process, so
only system types (UIImage, UIColor, AttributedString) are available — no SwiftUI views or
app-level dependencies.
Common pitfalls
-
Missing entitlement silently breaks everything. The
com.apple.developer.family-controlsentitlement requires a provisioning profile with the Family Controls capability. Without it,requestAuthorizationthrows no error but authorization status stays.notDeterminedforever. Check your entitlements file and re-sign the build. -
Simulator support is limited.
FamilyActivityPickerrenders in the simulator from iOS 16+, butManagedSettingsStoreshields do not actually block apps there. Always test restrictions on a real device, and use a secondary Apple ID / Family Sharing member account to test the.familyauthorization path. -
Extension targets must share an App Group.
DeviceActivityMonitorandShieldConfigurationDataSourcerun in separate processes. Any shared state (e.g., the user'sFamilyActivitySelection) must be persisted via a shared App Group container (UserDefaults(suiteName:)/FileManager) — not in-memory or in the main app's sandbox. -
VoiceOver cannot read app names.
applicationTokensare opaque — you cannot retrieve the app name or icon for display. Use the count of selected tokens plus an accessible label like "3 apps selected" rather than trying to list them, and rely onFamilyActivityPicker(which handles its own accessibility) for selection.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement screen time api in SwiftUI for iOS 17+. Use FamilyControls, FamilyActivityPicker, ManagedSettings, and DeviceActivityCenter. Create an @Observable model, a root NavigationStack view, and stub out the DeviceActivity Monitor Extension target. Make it accessible (VoiceOver labels on all controls). Add a #Preview with realistic sample data.
In the Soarias Build phase, paste this prompt into the implementation panel so Claude Code scaffolds all three targets — main app, monitor extension, and shield configuration extension — with the correct entitlements and App Group wiring before you write a single line manually.
Related
FAQ
Does this work on iOS 16?
The core FamilyControls and ManagedSettings frameworks
launched in iOS 15, and FamilyActivityPicker as a SwiftUI view arrived in
iOS 16. However, the @Observable macro used in this guide requires iOS 17.
If you need iOS 16 support, swap @Observable for
@MainActor class … : ObservableObject and annotate properties with
@Published. The Screen Time API surface itself is unchanged.
Can I read how many minutes a user spent in a blocked app?
Not directly from the main app. Usage data flows only into the
DeviceActivityMonitor extension via threshold events — you define a
DeviceActivityEvent with a usage threshold (e.g., 30 minutes), and the
system calls eventDidReachThreshold(_:activity:) in your extension when
the threshold is crossed. Raw minute-by-minute usage is never exposed to your code;
Apple deliberately keeps it opaque to protect privacy.
What's the UIKit equivalent?
There is no UIKit-specific Screen Time API — all three frameworks
(FamilyControls, ManagedSettings, DeviceActivity)
are UIKit-compatible. The only SwiftUI-exclusive piece is the
.familyActivityPicker(isPresented:selection:) view modifier. In a UIKit
app, present FamilyActivityPickerViewController modally and read the
resulting FamilyActivitySelection from its delegate callback instead.
Last reviewed: 2026-05-12 by the Soarias team.