Building social apps with the Screen Time API on iOS.
DeviceActivity, App Groups, and accountability.
If you've ever worked with iOS's Screen Time APIs, you know that the documentation is a bit sparse.
This writeup is an attempt to simplify programatic acess to screen time data in Swift as much as possible. It will also cover the basics of displaying activity reports, app usage icons, and extension debugging.
(as a bonus, check out Sharetime -- screen time with friends on iOS)
1. Family Controls Distribution Entitlement
The first step of developing with the Screen Time APIs is to apply for the Family Controls Distribution Entitlement. Well before publishing to TestFlight or the App Store, apply for the Family Controls entitlement at https://developer.apple.com/contact/request/family-controls-distribution. This is required for all app extensions that interact with Screen Time APIs.
You must submit the entitlement form up to three times for a single app:
- First with your main app's bundle ID
- Then with your DeviceActivityMonitorExtension's bundle ID (if you use this extension)
- Lastly with your DeviceActivityReportExtension's bundle ID (if you use this extension)
Getting approved for the entitlement can take up to 30 days, so apply as early as possible.
2. Fetching daily screen time
To track screen time, you first need to register activities with DeviceActivityCenter inside of your main app. This should be done after the user has already granted permission to access their screen time data, has selected the apps you want to track using a FamilyActivitySelection object, and has selected the time thresholds you want to track.
Here's how it works: create events that trigger at regular intervals throughout the day. In this example, we'll use 5-minute intervals (at 5, 10, 15 minutes, etc.) up to 1435 minutes (23h 55m). These events monitor specific apps you select through FamilyActivitySelection.
When a user reaches a time threshold, your DeviceActivityMonitorExtension receives an event (like “event.75.thresholdReach” for 75 minutes), which allows your app to get an estimate of the user's screen time for the day.
Note on precision: While this example uses 5-minute intervals, you can use intervals as precise as 1 minute for more granular tracking. However, be aware that the more activities you register, the longer it takes for the system to process them. With 1-minute intervals, you'd have 1440 events to register, which can significantly slow down the registration process.
Additionally, when the user first installs your app, no thresholds will be hit, so we should set includePastActivity to true to force events from the period before the app was installed to fire retroactively. An example implementation could look like this:
func startMonitoring(with selection: FamilyActivitySelection) async {
// Stop any ongoing monitoring
let center = DeviceActivityCenter()
center.stopMonitoring()
var events: [DeviceActivityEvent.Name: DeviceActivityEvent] = [:]
// Set up events for every 5 minutes from 5 to 1435 minutes (23h 55m)
for minute in stride(from: 5, through: 1435, by: 5).reversed() {
let eventName = DeviceActivityEvent.Name("event.\(minute).thresholdReach")
events[eventName] = DeviceActivityEvent(
applications: selection.applicationTokens,
categories: selection.categoryTokens,
webDomains: selection.webDomainTokens,
threshold: DateComponents(minute: minute),
includesPastActivity: true
)
}
// Create single daily schedule from 00:00:00 to 23:59:59
let startTime = DateComponents(hour: 0, minute: 0, second: 0)
let endTime = DateComponents(hour: 23, minute: 59, second: 59)
let schedule = DeviceActivitySchedule(
intervalStart: startTime,
intervalEnd: endTime,
repeats: true
)
let activity = DeviceActivityName("schedule.0")
do {
try center.startMonitoring(activity, during: schedule, events: events)
print("Started monitoring single daily schedule with \(events.count) events")
} catch {
print("Error starting monitoring: \(error)")
}
}Registering activities with center.startMonitoring() blocks the main UI thread, which freezes your app and makes it unresponsive to touch gestures. To avoid this, use a detached Task to prevent blocking the main thread when calling this function. For example:
Task.detached(priority: .userInitiated) {
await startMonitoring(with: selection)
}Activity registration can be slow depending on the number of activities you have, and registering many activities can take minutes to complete.
3. DeviceActivityMonitorExtension (DAMExtension)
After registering activities, you need to add a new target to your Xcode project called a DeviceActivityMonitorExtension. This extension is used to handle the callbacks from the registered activities, which are triggered when each usage threshold is reached.
Inside your DAMExtension, you can extract the minute value directly from the event's raw name like “event.75.thresholdReach” using event.rawValue.components(separatedBy: “.”)[1] to get 75 minutes, and compare it to previous max minutes to get current total. Also implement a daily reset mechanism to keep track of transitions between days.
Unlike DeviceActivityReport extensions, DeviceActivityMonitor extensions have support for network requests, allowing synchronization with external databases like Supabase. When each event fires, you can sync the data back to your remote database which can be used for social features like allowing users to see each other's screen time.
Two more things to note: the extension operates under strict memory constraints, and print() statements do not work within the DeviceActivityMonitorExtension, making debugging difficult. Keep your code lightweight and avoid heavy computations or large data structures. Use local push notifications to log events and debug instead.
Here's a basic implementation:
import DeviceActivity
import Foundation
import UserNotifications
// Optionally override any of the functions below.
class DeviceActivityMonitorExtension: DeviceActivityMonitor {
func debugNotification(with title: String) {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
guard granted else { return }
let content = UNMutableNotificationContent()
content.title = title
content.body = "via extension"
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
center.add(request)
}
}
override func intervalDidStart(for activity: DeviceActivityName) {
super.intervalDidStart(for: activity)
}
override func intervalDidEnd(for activity: DeviceActivityName) {
super.intervalDidEnd(for: activity)
}
// This is the function that is called when a threshold is reached
override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
super.eventDidReachThreshold(event, activity: activity)
let userDefaults = UserDefaults(suiteName: "group.com.yourapp.shared")
let today = DateFormatter.dateOnly.string(from: Date())
let lastResetDate = userDefaults?.string(forKey: "lastResetDate")
let eventComponents = event.rawValue.components(separatedBy: ".")
if eventComponents.count >= 2, let minuteIndex = Int(eventComponents[1]) {
var currentMaxMinutes = userDefaults?.integer(forKey: "currentMaxMinutes") ?? 0
if lastResetDate != today {
currentMaxMinutes = minuteIndex
userDefaults?.setValue(today, forKey: "lastResetDate")
} else {
currentMaxMinutes = max(currentMaxMinutes, minuteIndex)
}
userDefaults?.setValue(Double(currentMaxMinutes), forKey: "totalScreenMinutes")
// Sync to Supabase or backend here
...
}
}
}4. App Groups and fetching screen time in the main app
App Groups are essential because the DeviceActivityMonitorExtension runs in a separate process from your main app. Standard UserDefaults cannot share data between these isolated processes, making it impossible to pass screen time data from the extension to your app.
Use UserDefaults(suiteName:) with an App Group to create a shared data container. In the DAMExtension, we store the current total screen time with
let sharedDefaults = UserDefaults(suiteName: "group.com.yourapp.shared")
sharedDefaults?.setValue(Double(currentMaxMinutes), forKey: "totalScreenMinutes")In your main app, you can now access this data at any time using
let sharedDefaults = UserDefaults(suiteName: "group.com.yourapp.shared")
let totalMinutes = sharedDefaults?.double(forKey: "totalScreenMinutes") ?? 0.0
print("User's total screen time: \(totalMinutes) minutes")
// you can now use totalMinutes programmatically!Now, by using App Groups, we are able to access screen time programmatically from our main app using only two lines of code!
Lastly, make sure to enable App Groups in the Capabilities section of both the main app target and the DAMExtension target in Xcode, otherwise you will see null values when trying to access the data.
5. Displaying app icons in DeviceActivityReportExtension
One key feature of screen time tracking is the ability to display app icons for the apps that you are tracking. Unfortunately, there is not much documentation on how to do this. To do this, we can create a DeviceActivityReportExtension which uses SwiftUI Labels to display the app icons.
I recommend looking at the UsageDemo repo by pietromarvelli for a basic example of how to setup a working DeviceActivityReportExtension. The ScreenBreak repo by christianp-622 is also a good example of more advanced usage. After that, use the example code below to add the app icons to the TotalActivityView.swift file.
Make sure to import the FamilyControls framework, otherwise you will run into errors.
import FamilyControlsThen, use the Label view to display the app icon, and use the scaleEffect modifier to scale it to your desired size.
Label(app.icon).labelStyle(.iconOnly).scaleEffect(1.5, anchor: .center)Final thoughts
These frameworks have very limited documentation, hence what inspired me to write about this. I hope this can help you build something great.
Other good resources
- A curious way to grab Screen Time data on iOS by Sergio Mattei (DeviceActivityMonitor)
- Using Screen Time APIs in iOS by Sergio Mattei (DeviceActivityMonitor)
- A Developer's Guide to Apple's Screen Time APIs by Juliusbrussee
- UsageDemo repo by pietromarvelli (DeviceActivityReport)
- Using Screen Time API to block apps for a specified time by Pedro Esli
- DeviceActivityMonitor on Apple Developer Forums
- ScreenBreak repo by christianp-622
- DeviceActivityReport on Apple Developer Forums
