```html How to Build a Screen Time Tracker App in SwiftUI (2026)

How to Build a Screen Time Tracker App in SwiftUI

A Screen Time Tracker lets users visualize their iPhone app usage by category, day, and week — surfacing habits they can actually act on. It's aimed at productivity-focused users who want more insight than Apple's built-in Screen Time provides, without handing data to a cloud service.

iOS 17+ · Xcode 16+ · SwiftUI Complexity: Advanced Estimated time: 2–4 weeks Updated: May 12, 2026

Prerequisites

Architecture overview

The app is split across three targets: the main SwiftUI app, a DeviceActivityReport extension that reads usage data from the system, and a ManagedSettings extension for optional blocking. SwiftData persists aggregated daily snapshots in the app group container shared between targets. The main app requests FamilyControls authorization on launch, schedules a daily DeviceActivity monitor via DeviceActivityCenter, and renders Swift Charts from the aggregated store. StoreKit 2 drives the subscription paywall before the 7-day trial expires.

ScreenTimeTracker/
├── App/
│   ├── ScreenTimeTrackerApp.swift
│   ├── ContentView.swift
│   └── Models/UsageSnapshot.swift       ← SwiftData @Model
├── Features/
│   ├── Dashboard/DashboardView.swift
│   ├── Analytics/AnalyticsViewModel.swift
│   └── Paywall/PaywallView.swift
├── DeviceActivityExtension/             ← separate target
│   └── DeviceActivityReportExtension.swift
└── PrivacyInfo.xcprivacy
      

Step-by-step

1. Data model

Define a SwiftData @Model for daily usage snapshots stored in the shared App Group container so both the main app and the extension can read and write it.

import SwiftData
import Foundation

@Model
final class UsageSnapshot {
    var date: Date
    var bundleIdentifier: String
    var appName: String
    var categoryToken: String       // serialized ApplicationToken description
    var totalSeconds: Int
    var pickups: Int
    var notificationCount: Int

    init(
        date: Date,
        bundleIdentifier: String,
        appName: String,
        categoryToken: String,
        totalSeconds: Int,
        pickups: Int,
        notificationCount: Int
    ) {
        self.date = date
        self.bundleIdentifier = bundleIdentifier
        self.appName = appName
        self.categoryToken = categoryToken
        self.totalSeconds = totalSeconds
        self.pickups = pickups
        self.notificationCount = notificationCount
    }
}

// App Group container shared with DeviceActivity extension
let sharedModelContainer: ModelContainer = {
    let schema = Schema([UsageSnapshot.self])
    let config = ModelConfiguration(
        schema: schema,
        url: FileManager.default
            .containerURL(forSecurityApplicationGroupIdentifier: "group.com.yourteam.screentimetracker")!
            .appendingPathComponent("usage.store")
    )
    return try! ModelContainer(for: schema, configurations: [config])
}()

2. Core UI — usage dashboard

Build the main dashboard with a Swift Charts bar chart breaking down screen time by app, plus a weekly trend line — all driven from the SwiftData query.

import SwiftUI
import Charts
import SwiftData

struct DashboardView: View {
    @Query(sort: \UsageSnapshot.date, order: .reverse) private var snapshots: [UsageSnapshot]
    @State private var selectedRange: RangeOption = .today

    var filtered: [UsageSnapshot] {
        let cutoff = Calendar.current.startOfDay(for: selectedRange.cutoffDate)
        return snapshots.filter { $0.date >= cutoff }
    }

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(alignment: .leading, spacing: 24) {
                    Picker("Range", selection: $selectedRange) {
                        ForEach(RangeOption.allCases) { Text($0.label).tag($0) }
                    }
                    .pickerStyle(.segmented)

                    Chart(filtered) { snap in
                        BarMark(
                            x: .value("App", snap.appName),
                            y: .value("Minutes", snap.totalSeconds / 60)
                        )
                        .foregroundStyle(by: .value("App", snap.appName))
                    }
                    .frame(height: 260)
                    .chartXAxis(.hidden)

                    TotalSummaryRow(snapshots: filtered)
                }
                .padding()
            }
            .navigationTitle("Screen Time")
        }
    }
}

enum RangeOption: String, CaseIterable, Identifiable {
    case today, week, month
    var id: String { rawValue }
    var label: String { rawValue.capitalized }
    var cutoffDate: Date {
        switch self {
        case .today:  return Date()
        case .week:   return Calendar.current.date(byAdding: .day, value: -7, to: Date())!
        case .month:  return Calendar.current.date(byAdding: .month, value: -1, to: Date())!
        }
    }
}

3. App usage analytics — FamilyControls + DeviceActivityReport

Request FamilyControls authorization, schedule a daily DeviceActivity monitor, and implement the extension that receives usage callbacks and persists snapshots to the shared SwiftData store.

// In main app — authorization + scheduling
import FamilyControls
import DeviceActivity

class UsageMonitorManager: ObservableObject {
    let center = DeviceActivityCenter()

    func requestAuthorizationAndSchedule() async {
        do {
            try await AuthorizationCenter.shared
                .requestAuthorization(for: .individual)
            scheduleMonitor()
        } catch {
            print("FamilyControls auth failed: \(error)")
        }
    }

    private func scheduleMonitor() {
        let schedule = DeviceActivitySchedule(
            intervalStart: DateComponents(hour: 0, minute: 0),
            intervalEnd: DateComponents(hour: 23, minute: 59),
            repeats: true
        )
        try? center.startMonitoring(
            .daily,
            during: schedule
        )
    }
}

// DeviceActivityExtension target
import DeviceActivity
import SwiftData

class ReportExtension: DeviceActivityReportExtension {
    override func intervalDidEnd(for activity: DeviceActivityName) {
        // Fetch system-provided usage via DeviceActivityReport API
        // Write aggregated UsageSnapshot objects to the shared ModelContainer
        let container = sharedModelContainer
        let context = ModelContext(container)
        // ... map DeviceActivityResults to UsageSnapshot and insert
        try? context.save()
    }
}

4. Privacy Manifest setup

App Store review requires a PrivacyInfo.xcprivacy that declares FamilyControls usage, any required reason APIs you call (e.g. NSPrivacyAccessedAPICategoryFileTimestamp), and confirms no data leaves the device.

<!-- PrivacyInfo.xcprivacy (Property List source) -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>NSPrivacyTracking</key><false/>
  <key>NSPrivacyTrackingDomains</key><array/>
  <key>NSPrivacyCollectedDataTypes</key><array/>
  <key>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array><string>C617.1</string></array>
    </dict>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array><string>CA92.1</string></array>
    </dict>
  </array>
</dict>
</plist>

Common pitfalls

Adding monetization: Subscription

Use StoreKit 2's Product.products(for:) to load a monthly or annual auto-renewable subscription configured in App Store Connect. Gate the full analytics history (beyond 7 days) and any export features behind an @AppStorage-backed entitlement flag that you set after verifying the Transaction.currentEntitlement(for:) on every app launch. Display a PaywallView using Product.purchase() and listen on Transaction.updates as an async sequence so renewals and family-sharing grants are handled without polling. Offer a 7-day free trial via the subscription's introductory offer — users tracking habits need enough time to see a full week of data before committing.

Shipping this faster with Soarias

Soarias scaffolds the multi-target Xcode project for you — main app, DeviceActivityReport extension, and ManagedSettings extension — with the correct App Group identifiers already wired together. It also generates the PrivacyInfo.xcprivacy from a checklist, pre-fills all required App Store Connect metadata (privacy nutrition labels, export compliance, FamilyControls declaration), sets up fastlane with a Matchfile for all three provisioning profiles, and drives the ASC submission including the FamilyControls entitlement attachment that first-time submitters routinely forget.

For an advanced app like this one — three targets, a sensitive entitlement, and a subscription paywall — the manual setup typically burns 3–5 days of configuration before you write a single line of product code. Soarias compresses that to under two hours, letting you spend the full 2–4 weeks on the analytics and chart work that actually differentiates your app.

Related guides

FAQ

Do I need a paid Apple Developer account?

Yes. The FamilyControls entitlement, TestFlight distribution, and App Store submission all require an active Apple Developer Program membership ($99/year). You also need to separately request the FamilyControls entitlement from the developer portal — it does not activate automatically when you enroll.

How do I submit this to the App Store?

Archive all three targets together in Xcode (Product → Archive), then upload via Xcode Organizer or xcrun altool. In App Store Connect, attach the FamilyControls entitlement to the version, complete the privacy nutrition labels declaring no data collected, and confirm export compliance. Expect a standard 24–48 hour review cycle, though first submissions with FamilyControls can take longer if the reviewer needs to verify the entitlement approval.

Can I access usage data for other family members' devices?

Only if the user's device is the Family Organizer and you use AuthorizationCenter.shared.requestAuthorization(for: .individual) for the local device, or .family for managed child accounts. You cannot pull another adult family member's usage data — Apple's privacy model explicitly prevents it. Attempting to advertise cross-family monitoring in your App Store listing is a common rejection reason.

Last reviewed: 2026-05-12 by the Soarias team.

```