```html SwiftUI: How to Build Screen Time Controls (iOS 17+, 2026)

How to Build Screen Time Controls in SwiftUI

iOS 17+ Xcode 16+ Advanced APIs: FamilyControls Updated: May 11, 2026
TL;DR

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

  1. Entitlement gate. Before any API call succeeds, you must add the Family Controls capability in Xcode → Signing & Capabilities and choose the Individual entitlement type. Without it, requestAuthorization always throws an AuthorizationError.
  2. Authorization flow. AuthorizationCenter.shared.requestAuthorization(for: .individual) on line 18 shows a one-time system alert. On success it sets isAuthorized = true; subsequent calls return immediately if already granted.
  3. Privacy-preserving picker. The .familyActivityPicker(isPresented:selection:) modifier on line 87 presents an Apple-managed sheet. The system populates FamilyActivitySelection with opaque Application tokens — your process never sees raw bundle IDs or app names in plain text.
  4. Blocking via ManagedSettingsStore. Writing to store.application.blockedApplications and store.application.blockedApplicationCategories in applyRestrictions() immediately prevents those apps from launching. The store persists settings until clearAllSettings() is called.
  5. Task-scoped re-check. The .task modifier re-runs requestAuthorization() 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

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.

```