Meet the Screen Time API
System & Services 进阶 20m

认识屏幕使用时间 API

Meet the Screen Time API

2021年6月9日

在 Apple 官方观看视频

一句话判断

Screen Time API 终于开放给第三方开发者 —— 家庭教育类 app 可以读取孩子的使用时长、设置 App 限额和内容过滤,但权限管控非常严格,你的 app 必须明确声明用途且通过审核才能使用。

这场 Session 讲了什么

iOS 15 首次向开发者开放了 Screen Time API,允许授权的第三方 app 读取和管理屏幕使用时间数据。API 分为三个核心模块:

  1. FamilyActivitySelection(家庭活动选择):让用户选择哪些 app 和网页类别纳入管理。系统提供一个标准的 picker 界面,用户选择后返回一个 token。
  2. DeviceActivityMonitor(设备活动监控):监听设备使用事件,如在某个时间段内使用了特定类别的 app。可以设置阈值触发回调。
  3. ManagedSettings(受管设置):根据 FamilyActivitySelection 的选择,应用限制:屏蔽特定 app、限制网页内容、阻止更改系统设置等。

整个 API 围绕”家长控制”场景设计,但也适用于个人效率管理 app。权限模型基于 Family Controls entitlement,需要在 Apple Developer 网站申请。

值得深挖的点

权限模型的三层保护。你的 app 要使用 Screen Time API,需要:(1) 在 Apple Developer 后台申请 Family Controls entitlement;(2) 在 Info.plist 中声明 FamilyControls 用途描述;(3) 用户在运行时通过系统弹窗显式授权。第三步的弹窗是系统级 UI(和 ATT 类似),用户可以随时在设置中撤销。这意味着你的 app 不能偷偷读取使用数据。

DeviceActivityMonitor 的后台执行DeviceActivityMonitor 的回调(如 intervalDidStartintervalDidEndeventDidReachThreshold)在后台执行,不是在 app 进程内。系统会唤醒你的 app extension(DeviceActivityMonitorExtension)来处理这些事件。这个设计确保了即使 app 被杀掉,监控仍然有效。但 extension 的执行时间有限(通常几秒),不能做耗时操作。

代码片段

请求 Family Controls 权限

import FamilyControls

class ScreenTimeManager: ObservableObject {
    @Published var isAuthorized = false

    func requestAuthorization() async {
        do {
            // 请求 Family Controls 权限
            try await AuthorizationCenter.shared.requestAuthorization(for: .child)
            isAuthorized = true
        } catch {
            print("Family Controls 授权失败: \(error)")
            isAuthorized = false
        }
    }

    // 在 app 启动时检查当前授权状态
    func checkAuthorization() {
        switch AuthorizationCenter.shared.authorizationStatus {
        case .approved:
            isAuthorized = true
        case .denied:
            isAuthorized = false
        case .notDetermined:
            // 还未请求过权限
            break
        @unknown default:
            break
        }
    }
}

展示 App 选择器

import SwiftUI
import FamilyControls

struct AppPickerView: View {
    @State private var selection = FamilyActivitySelection()
    @State private var showPicker = false

    var body: some View {
        VStack {
            Text("选择要限制的 App 和网页类别")
                .font(.headline)

            Button("选择 App") {
                showPicker = true
            }
            .familyActivityPicker(isPresented: $showPicker,
                                  selection: $selection)

            // 显示已选择的类别数量
            if !selection.applicationTokens.isEmpty {
                Text("已选择 \(selection.applicationTokens.count) 个 App")
            }
            if !selection.categoryTokens.isEmpty {
                Text("已选择 \(selection.categoryTokens.count) 个类别")
            }

            Button("应用限制") {
                applyRestrictions(selection)
            }
            .disabled(selection.applicationTokens.isEmpty && selection.categoryTokens.isEmpty)
        }
    }
}

应用使用限制

import FamilyControls
import ManagedSettings

class RestrictionManager {
    let store = ManagedSettingsStore()

    func applyRestrictions(_ selection: FamilyActivitySelection) {
        // 屏蔽选中的 app
        store.shield.applications = selection.applicationTokens
        // 屏蔽选中的网页类别
        store.shield.webContent = selection.categoryTokens
        // 可选:阻止 App Store 安装新 app
        store.appStore.requiresPassword = true
        // 可选:阻止更改系统设置
        store.account.lockAccounts = true

        print("限制已应用")
    }

    func removeRestrictions() {
        // 清除所有限制
        store.clearAllSettings()
        print("限制已移除")
    }
}

设备活动监控 Extension

import DeviceActivity

// 在 DeviceActivityMonitorExtension 中实现回调
class DeviceActivityMonitorExtension: DeviceActivityMonitor {
    override func intervalDidStart(for activity: DeviceActivityName) {
        super.intervalDidStart(for: activity)
        // 用户开始使用被监控的 app
        print("监控周期开始: \(activity)")
    }

    override func intervalDidEnd(for activity: DeviceActivityName) {
        super.intervalDidEnd(for: activity)
        // 监控周期结束(如"学习时间"结束)
        print("监控周期结束: \(activity)")
    }

    override func eventDidReachThreshold(
        _ event: DeviceActivityEvent,
        activity: DeviceActivityName
    ) {
        super.eventDidReachThreshold(event, activity: activity)
        // 使用时长达到阈值(如使用社交媒体 30 分钟)
        print("达到阈值: \(event)")
        // 发送本地通知提醒用户
        sendUsageReminder()
    }
}

最佳实践

  1. 权限请求要在功能上下文中发起。不要在 app 启动时立即弹出 Family Controls 授权。在用户点击”开始管理屏幕时间”按钮后再请求,通过率会高很多。
  2. 选择器 UI 使用系统提供的 familyActivityPicker。不要自己实现 app 列表,系统 picker 会自动展示已安装 app 的图标和名称,且保证隐私安全。
  3. 限制策略要有”宽限期”。不要一次性严格限制所有社交 app。逐步收紧比一刀切更容易被用户(尤其是孩子)接受。

还有什么值得关注

  • Screen Time API 对 app 类别的 token 是不透明的(ApplicationToken),你不能通过它知道具体是哪个 app,只能用于设置限制。这是隐私保护设计。
  • ManagedSettingsStore 支持多个独立 store,适合”学习时间”、“睡眠时间”等不同场景的差异化配置。
  • 这个 API 在 MDM(移动设备管理)环境中也有用途,企业可以用它限制员工设备上的非工作 app。
WWDC 2021