```html SwiftUI: Screen Time API (iOS 17+, 2026)

How to Build Screen Time API in SwiftUI

iOS 17+ Xcode 16+ Advanced APIs: ScreenTime (FamilyControls, ManagedSettings, DeviceActivity) Updated: May 12, 2026
TL;DR

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

  1. Authorization (requestAuthorization(for:)) — called inside .task on the root view so the system prompt appears once at launch. .individual is for users restricting themselves; use .family when a parent manages a child's device via iCloud Family Sharing. The status is reflected in model.authStatus to hide the UI until approved.
  2. 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 into FamilyActivitySelection, which carries opaque applicationTokens and webDomainTokens that never expose bundle IDs directly.
  3. Applying shields (ManagedSettingsStore) — setting store.shield.applications to a non-nil set of tokens immediately shows the Screen Time shield over those apps. Setting it back to nil lifts the restriction. clearAllSettings() wipes every managed setting in one call.
  4. @Observable model — using the iOS 17 Observation framework keeps the view in sync with auth status and restriction state without manual objectWillChange calls. ScreenTimeModel is injected via @State at the root and can be passed down with @Environment in deeper views.
  5. Extension target for monitoringDeviceActivityMonitor subclasses must live in a DeviceActivity Monitor Extension target (File → New Target in Xcode). The main app calls DeviceActivityCenter().startMonitoring(_:during:) with a DeviceActivitySchedule to 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

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.

```