diff --git a/AreYouSleeping/AreYouSleeping.entitlements b/AreYouSleeping/AreYouSleeping.entitlements
new file mode 100644
index 0000000..268f5fc
--- /dev/null
+++ b/AreYouSleeping/AreYouSleeping.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.developer.family-controls
+
+ com.apple.security.application-groups
+
+ group.com.areyousleeping
+
+
+
diff --git a/AreYouSleeping/AreYouSleepingApp.swift b/AreYouSleeping/AreYouSleepingApp.swift
index 537ab07..563e8a8 100644
--- a/AreYouSleeping/AreYouSleepingApp.swift
+++ b/AreYouSleeping/AreYouSleepingApp.swift
@@ -3,6 +3,7 @@ import SwiftUI
@main
struct AreYouSleepingApp: App {
@StateObject private var store = Store.shared
+ @StateObject private var stm = ScreenTimeManager.shared
var body: some Scene {
WindowGroup {
@@ -13,6 +14,10 @@ struct AreYouSleepingApp: App {
.onAppear {
BedtimeReminder.requestPermission()
BedtimeReminder.schedule(store: store)
+ // 请求屏幕时间权限(用于应用屏蔽)
+ Task {
+ await stm.requestAuthorization()
+ }
}
} else {
AgreementView()
diff --git a/AreYouSleeping/Services/ScreenTimeManager.swift b/AreYouSleeping/Services/ScreenTimeManager.swift
new file mode 100644
index 0000000..0c29f30
--- /dev/null
+++ b/AreYouSleeping/Services/ScreenTimeManager.swift
@@ -0,0 +1,105 @@
+import Foundation
+import FamilyControls
+import DeviceActivity
+import ManagedSettings
+import SwiftUI
+
+/// 屏幕时间管理器 — 授权、应用选择、定时屏蔽
+@MainActor
+final class ScreenTimeManager: ObservableObject {
+ static let shared = ScreenTimeManager()
+
+ @Published var isAuthorized = false
+ @Published var selection = FamilyActivitySelection()
+ @Published var blockedAppCount = 0
+
+ private let center = DeviceActivityCenter()
+ private let shieldStore = ManagedSettingsStore()
+ private let sharedDefaults = UserDefaults(suiteName: "group.com.areyousleeping")!
+
+ private init() {
+ refreshAuth()
+ loadSelection()
+ }
+
+ // MARK: - 授权
+
+ func requestAuthorization() async {
+ do {
+ try await AuthorizationCenter.shared.requestAuthorization(for: .individual)
+ isAuthorized = true
+ } catch {
+ print("ScreenTime auth failed: \(error)")
+ isAuthorized = false
+ }
+ }
+
+ func refreshAuth() {
+ isAuthorized = AuthorizationCenter.shared.authorizationStatus == .approved
+ }
+
+ // MARK: - 选择屏蔽应用
+
+ func saveSelection(_ sel: FamilyActivitySelection) {
+ selection = sel
+ blockedAppCount = sel.applicationTokens.count + sel.categoryTokens.count
+
+ // 持久化到 App Group,供 Extension 读取
+ let dict: [String: Any] = ["appCount": blockedAppCount]
+ sharedDefaults.set(dict, forKey: "shield_info")
+
+ // 直接写入 ManagedSettings(即使 App 被杀也生效)
+ shieldStore.shield.applications = sel.applicationTokens.isEmpty ? nil : sel.applicationTokens
+ shieldStore.shield.applicationCategories = sel.categoryTokens.isEmpty
+ ? nil
+ : .specific(sel.categoryTokens)
+
+ // 根据当前作息调度 DeviceActivity
+ let store = Store.shared
+ scheduleMonitoring(bedMin: store.bedtimeMinutes, wakeMin: store.waketimeMinutes)
+ }
+
+ private func loadSelection() {
+ blockedAppCount = sharedDefaults.integer(forKey: "shield_count")
+ }
+
+ // MARK: - DeviceActivity 调度
+
+ func scheduleMonitoring(bedMin: Int, wakeMin: Int) {
+ center.stopAllMonitoring()
+ guard blockedAppCount > 0 else { return }
+
+ let schedule = DeviceActivitySchedule(
+ intervalStart: DateComponents(hour: bedMin / 60, minute: bedMin % 60),
+ intervalEnd: DateComponents(hour: wakeMin / 60, minute: wakeMin % 60),
+ repeats: true
+ )
+ do {
+ try center.startMonitoring(.sleep, during: schedule)
+ } catch {
+ print("DeviceActivity start fail: \(error)")
+ }
+ }
+
+ func stopMonitoring() {
+ center.stopAllMonitoring()
+ }
+
+ // MARK: - 手动开关屏蔽
+
+ func applyShieldNow() {
+ shieldStore.shield.applications = selection.applicationTokens.isEmpty ? nil : selection.applicationTokens
+ shieldStore.shield.applicationCategories = selection.categoryTokens.isEmpty
+ ? nil
+ : .specific(selection.categoryTokens)
+ }
+
+ func removeShieldNow() {
+ shieldStore.shield.applications = nil
+ shieldStore.shield.applicationCategories = nil
+ }
+}
+
+extension DeviceActivityName {
+ static let sleep = Self("com.areyousleeping.sleep")
+}
diff --git a/AreYouSleeping/Store.swift b/AreYouSleeping/Store.swift
index fa1f767..d3eb855 100644
--- a/AreYouSleeping/Store.swift
+++ b/AreYouSleeping/Store.swift
@@ -31,8 +31,9 @@ final class Store: ObservableObject {
@Published var sleepStartMillis: Double = 0
@Published var lastNagMillis: Double = 0
- // MARK: - 屏蔽应用包名(iOS 不支持实际屏蔽,仅记录)
- @Published var blockedPackages: Set = []
+ // MARK: - 屏蔽应用(Screen Time API)
+ @Published var blockedAppCount: Int = 0 // 已选中的应用数
+ @Published var familyControlsAuthorized: Bool = false
// MARK: - 睡眠记录
@Published var recordLines: Set = []
@@ -65,7 +66,7 @@ final class Store: ObservableObject {
isSleeping = d.bool(forKey: "sleeping")
sleepStartMillis = d.double(forKey: "start")
lastNagMillis = d.double(forKey: "lastNag")
- blockedPackages = Set(d.stringArray(forKey: "blocked") ?? [])
+ blockedAppCount = d.integer(forKey: "blocked_count")
recordLines = Set(d.stringArray(forKey: "records") ?? [])
}
@@ -91,7 +92,7 @@ final class Store: ObservableObject {
d.set(isSleeping, forKey: "sleeping")
d.set(sleepStartMillis, forKey: "start")
d.set(lastNagMillis, forKey: "lastNag")
- d.set(Array(blockedPackages), forKey: "blocked")
+ d.set(blockedAppCount, forKey: "blocked_count")
d.set(Array(recordLines), forKey: "records")
}
diff --git a/AreYouSleeping/Views/SettingsView.swift b/AreYouSleeping/Views/SettingsView.swift
index db11178..8801232 100644
--- a/AreYouSleeping/Views/SettingsView.swift
+++ b/AreYouSleeping/Views/SettingsView.swift
@@ -1,11 +1,14 @@
import SwiftUI
+import FamilyControls
struct SettingsView: View {
@EnvironmentObject var store: Store
+ @StateObject private var stm = ScreenTimeManager.shared
@State private var showAgreement = false
@State private var showTimePicker = false
@State private var isBedPicker = false
@State private var pickerDate = Date()
+ @State private var showFamilyPicker = false
var body: some View {
NavigationStack {
@@ -62,6 +65,35 @@ struct SettingsView: View {
.onChange(of: store.supervisionEnabled) { _ in store.save() }
}
+ // 应用屏蔽卡片
+ cardSection(header: "应用屏蔽", footer: "就寝时段通过系统屏幕时间屏蔽所选应用") {
+ VStack(spacing: 12) {
+ HStack {
+ Text(stm.blockedAppCount > 0
+ ? "已选择 \(stm.blockedAppCount) 个应用"
+ : "未选择任何应用")
+ .font(.system(size: 14))
+ Spacer()
+ Button("选择应用") {
+ showFamilyPicker = true
+ }
+ .buttonStyle(.bordered)
+ .tint(.blue)
+ .disabled(!stm.isAuthorized)
+ }
+ if !stm.isAuthorized {
+ HStack(spacing: 6) {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .font(.caption)
+ .foregroundColor(.orange)
+ Text("需先授予屏幕时间权限")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
// 后台运行卡片
cardSection(header: "后台运行", footer: "关闭后应用切到后台将停止服务") {
Toggle("允许后台运行", isOn: $store.backgroundRunning)
@@ -112,6 +144,17 @@ struct SettingsView: View {
timePickerSheet
.presentationDetents([.height(300)])
}
+ .familyActivityPicker(
+ isPresented: $showFamilyPicker,
+ selection: $stm.selection
+ )
+ .onChange(of: stm.selection) { newValue in
+ stm.saveSelection(newValue)
+ store.blockedAppCount = stm.blockedAppCount
+ store.save()
+ // 根据当前作息重新调度
+ stm.scheduleMonitoring(bedMin: store.bedtimeMinutes, wakeMin: store.waketimeMinutes)
+ }
}
}
diff --git a/DeviceActivityMonitor/DeviceActivityMonitor.swift b/DeviceActivityMonitor/DeviceActivityMonitor.swift
new file mode 100644
index 0000000..b14ab36
--- /dev/null
+++ b/DeviceActivityMonitor/DeviceActivityMonitor.swift
@@ -0,0 +1,49 @@
+import DeviceActivity
+import ManagedSettings
+
+/// 就寝时段屏蔽扩展 — 系统级回调,不依赖主 App 存活
+@main
+final class SleepMonitor: DeviceActivityMonitor {
+
+ private let store: ManagedSettingsStore = {
+ ManagedSettingsStore()
+ }()
+
+ // MARK: - 就寝开始 → 屏蔽应用
+
+ override func intervalDidStart(for activity: DeviceActivityName) {
+ super.intervalDidStart(for: activity)
+ applyShield()
+ }
+
+ // MARK: - 就寝结束 → 解除屏蔽
+
+ override func intervalDidEnd(for activity: DeviceActivityName) {
+ super.intervalDidEnd(for: activity)
+ removeShield()
+ }
+
+ // MARK: - 屏蔽事件回调(用户尝试打开被屏蔽应用时触发)
+
+ override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
+ super.eventDidReachThreshold(event, activity: activity)
+ // 阈值事件:可用于统计/通知
+ }
+
+ // MARK: - Private
+
+ private func applyShield() {
+ let shared = UserDefaults(suiteName: "group.com.areyousleeping")!
+ let count = shared.integer(forKey: "shield_count")
+ guard count > 0 else { return }
+
+ // ManagedSettings 屏蔽由主 App 写入后持久生效,
+ // Extension 的存在确保设备重启 / App 被回收后仍能正确触发解除。
+ // 此处可扩展添加自定义逻辑(如记录屏蔽次数等)。
+ }
+
+ private func removeShield() {
+ store.shield.applications = nil
+ store.shield.applicationCategories = nil
+ }
+}
diff --git a/DeviceActivityMonitor/Info.plist b/DeviceActivityMonitor/Info.plist
new file mode 100644
index 0000000..62df884
--- /dev/null
+++ b/DeviceActivityMonitor/Info.plist
@@ -0,0 +1,33 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ DeviceActivityMonitor
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSExtension
+
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).SleepMonitor
+ NSExtensionPointIdentifier
+ com.apple.deviceactivity.monitor
+
+ MinimumOSVersion
+ 16.0
+
+
diff --git a/ExportOptions.plist b/ExportOptions.plist
new file mode 100644
index 0000000..a149411
--- /dev/null
+++ b/ExportOptions.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ method
+ development
+ signingStyle
+ manual
+
+