How to Build Screen Time Controls in SwiftUI
Request authorization with AuthorizationCenter.shared.requestAuthorization(for: .individual), present a FamilyActivityPicker to let the user choose apps, then write the result to a ManagedSettingsStore to block those apps immediately.
import FamilyControls
import ManagedSettings
// 1. Authorize
try await AuthorizationCenter.shared
.requestAuthorization(for: .individual)
// 2. Collect a selection (wire to FamilyActivityPicker)
@State var selection = FamilyActivitySelection()
// 3. Block the chosen apps
let store = ManagedSettingsStore()
store.application.blockedApplications = selection.applications
store.application.blockedApplicationCategories =
.specific(selection.categories)
Full implementation
The implementation separates concerns into an @Observable view-model that owns the ManagedSettingsStore and the authorization lifecycle, and a SwiftUI view that drives the picker and displays current status. The picker itself is presented via the .familyActivityPicker view modifier — Apple's system sheet that renders app icons without leaking identifiers to your process. Restrictions applied to the store take effect immediately across the device for the authorized member.
import SwiftUI
import FamilyControls
import ManagedSettings
import DeviceActivity
// MARK: - View Model
@MainActor
@Observable
final class ScreenTimeViewModel {
var selection = FamilyActivitySelection()
var isPickerPresented = false
var isAuthorized = false
var errorMessage: String?
private let store = ManagedSettingsStore()
// Request Screen Time authorization from the OS.
// Must be called on MainActor; shows a system alert.
func requestAuthorization() async {
do {
try await AuthorizationCenter.shared
.requestAuthorization(for: .individual)
isAuthorized = true
errorMessage = nil
} catch {
isAuthorized = false
errorMessage = error.localizedDescription
}
}
// Push the chosen apps and categories into ManagedSettingsStore.
// Takes effect immediately; persists across app launches.
func applyRestrictions() {
store.application.blockedApplications = selection.applications
store.application.blockedApplicationCategories =
.specific(selection.categories)
}
// Remove all managed settings and reset the local selection.
func clearRestrictions() {
store.clearAllSettings()
selection = FamilyActivitySelection()
}
var selectedCount: Int {
selection.applications.count + selection.categories.count
}
}
// MARK: - Main View
struct ScreenTimeControlsView: View {
@State private var viewModel = ScreenTimeViewModel()
var body: some View {
NavigationStack {
List {
// Authorization section
Section {
if viewModel.isAuthorized {
Label("Screen Time access granted",
systemImage: "checkmark.shield.fill")
.foregroundStyle(.green)
.accessibilityLabel("Screen Time authorization granted")
} else {
Button {
Task { await viewModel.requestAuthorization() }
} label: {
Label("Request Screen Time Access",
systemImage: "person.badge.shield.checkmark")
}
.accessibilityHint("Opens a system dialog to grant permission")
}
if let msg = viewModel.errorMessage {
Text(msg)
.font(.caption)
.foregroundStyle(.red)
}
} header: {
Text("Authorization")
}
// App / category picker
Section {
Button {
viewModel.isPickerPresented = true
} label: {
Label("Choose Apps & Categories",
systemImage: "square.grid.2x2")
}
.disabled(!viewModel.isAuthorized)
.accessibilityHint("Opens the Family Activity Picker")
if viewModel.selectedCount > 0 {
Text("\(viewModel.selectedCount) item(s) selected")
.foregroundStyle(.secondary)
.font(.subheadline)
}
} header: {
Text("What to Restrict")
}
// Actions
Section {
Button("Apply Restrictions") {
viewModel.applyRestrictions()
}
.disabled(viewModel.selectedCount == 0)
.accessibilityHint("Blocks the selected apps on this device")
Button("Clear All Restrictions", role: .destructive) {
viewModel.clearRestrictions()
}
.accessibilityHint("Removes all active app blocks")
}
}
.navigationTitle("Screen Time Controls")
.navigationBarTitleDisplayMode(.large)
// System-provided picker — Apple renders icons; your app never
// sees the raw bundle identifiers of selected apps.
.familyActivityPicker(
isPresented: $viewModel.isPickerPresented,
selection: $viewModel.selection
)
.task {
// Silently re-check authorization on every appearance.
await viewModel.requestAuthorization()
}
}
}
}
// MARK: - Preview
#Preview {
ScreenTimeControlsView()
}
How it works
-
Entitlement gate. Before any API call succeeds, you must add the Family Controls capability in Xcode → Signing & Capabilities and choose the
Individualentitlement type. Without it,requestAuthorizationalways throws anAuthorizationError. -
Authorization flow.
AuthorizationCenter.shared.requestAuthorization(for: .individual)on line 18 shows a one-time system alert. On success it setsisAuthorized = true; subsequent calls return immediately if already granted. -
Privacy-preserving picker. The
.familyActivityPicker(isPresented:selection:)modifier on line 87 presents an Apple-managed sheet. The system populatesFamilyActivitySelectionwith opaqueApplicationtokens — your process never sees raw bundle IDs or app names in plain text. -
Blocking via ManagedSettingsStore. Writing to
store.application.blockedApplicationsandstore.application.blockedApplicationCategoriesinapplyRestrictions()immediately prevents those apps from launching. The store persists settings untilclearAllSettings()is called. -
Task-scoped re-check. The
.taskmodifier re-runsrequestAuthorization()each time the view appears, so the UI stays in sync if the user revokes access in the Settings app between launches.
Variants
Schedule a timed block with DeviceActivityCenter
Instead of an always-on block, use DeviceActivityCenter to restrict apps only during a window — for example, homework hours from 3 PM to 6 PM.
import DeviceActivity
let center = DeviceActivityCenter()
// Define a recurring daily schedule
let schedule = DeviceActivitySchedule(
intervalStart: DateComponents(hour: 15, minute: 0), // 3:00 PM
intervalEnd: DateComponents(hour: 18, minute: 0), // 6:00 PM
repeats: true
)
// Name this activity so your DeviceActivityMonitor extension can react
let activityName = DeviceActivityName("homework")
do {
try center.startMonitoring(activityName, during: schedule)
} catch {
print("Could not start monitoring: \(error)")
}
// In your DeviceActivityMonitor extension (separate target):
// override func intervalDidStart(for activity: DeviceActivityName) {
// let store = ManagedSettingsStore()
// store.application.blockedApplications = savedSelection.applications
// }
// override func intervalDidEnd(for activity: DeviceActivityName) {
// ManagedSettingsStore().clearAllSettings()
// }
Parental (Child) authorization mode
If your app manages a child's device from a parent device, use .requestAuthorization(for: .child) instead of .individual. This requires the parent to authenticate via Family Sharing, and Apple validates the relationship server-side. The rest of the ManagedSettingsStore API is identical; restrictions apply to the child's device rather than the current one.
Common pitfalls
-
Missing entitlement crashes silently on device. FamilyControls APIs are no-ops in Simulator and throw cryptic
AuthorizationError.insufficientAuthorizationon device when the entitlement is absent or mis-typed. Always test on physical hardware with a provisioning profile that includes thecom.apple.developer.family-controlsentitlement. -
FamilyActivityPicker cannot appear inside a sheet. Presenting the picker from inside another sheet or full-screen cover causes a silent failure on iOS 17. Drive
isPickerPresentedfrom a top-level view, not a nested one, and avoid layering modal presentations. - ManagedSettingsStore blocks persist after uninstall until reboot. If the user deletes your app while restrictions are active, the settings remain in effect until the device reboots or the user toggles Screen Time off and on. Always expose a clear "Remove All Restrictions" action and document this behaviour in your App Store description to avoid App Review rejection.
-
DeviceActivityMonitor must be a separate app extension target. The monitoring callbacks (
intervalDidStart,eventDidReachThreshold) run in anNSExtensionprocess, not your main app. Trying to applyManagedSettingsStorefrom the main app on a timer is unreliable; create the extension target in Xcode and link it to the same App Group.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement screen time controls in SwiftUI for iOS 17+. Use FamilyControls, FamilyActivityPicker, ManagedSettingsStore, and DeviceActivityCenter. Make it accessible (VoiceOver labels and hints on every button). Separate business logic into an @Observable view model. Add a DeviceActivityMonitor extension stub with intervalDidStart/End. Add a #Preview with realistic sample data and an authorized mock state.
In Soarias' Build phase, paste this prompt into the active session for your FamilyControls feature module — Soarias will scaffold the extension target and entitlements file alongside the SwiftUI view, keeping everything in sync with your project's existing signing identity.
Related
FAQ
Does this work on iOS 16?
FamilyActivityPicker and the ManagedSettings framework were introduced in iOS 15, but the @Observable macro and several DeviceActivityCenter APIs used in this guide require iOS 17+. You can back-port to iOS 16 by replacing @Observable with @MainActor ObservableObject / @Published, but Soarias targets iOS 17+ and the code as written will not compile below that deployment target.
Can I read which apps the user selected, or display their names?
No — by design. FamilyActivitySelection.applications returns a Set<ApplicationToken> of opaque tokens. You cannot reverse them to bundle identifiers or app names in your process. You can persist the entire FamilyActivitySelection via Codable (it conforms) and pass it back to ManagedSettingsStore, but you cannot display "YouTube" or "TikTok" by name in your own UI. This is a deliberate privacy constraint enforced by the OS.
What is the UIKit equivalent?
There is no pure UIKit equivalent — FamilyActivityPicker ships only as a SwiftUI view. In a UIKit app you would embed it via UIHostingController: let host = UIHostingController(rootView: FamilyActivityPickerWrapper()) and present host modally. The ManagedSettingsStore and AuthorizationCenter APIs are framework-level and work equally from UIKit or SwiftUI code.
Last reviewed: 2026-05-11 by the Soarias team.