- Shared/ layer compiled into the app and three new extension targets: rule snapshots in the app group, the usage ledger, monitoring-plan naming, and LimitEnforcement (shared, unit-tested event reactions) - RuleScheduler mirrors rules to the app group and reconciles DeviceActivity monitoring: one daily 00:00-23:59 activity per limit rule, with a cumulative usage-threshold event per budget minute for time limits; activities restart only when their configuration changes (a restart resets threshold accounting) - OpenAppLockMonitor (DeviceActivityMonitor): midnight budget resets, records usage minutes, shields at the budget, re-shields when a granted open session ends - OpenAppLockShieldConfig: open-limit shields show 'Opened X of N times today' with an 'Open (Y left)' secondary button - OpenAppLockShieldAction: an Open press spends one open, lifts the rule's shield, and starts the ~15-minute one-shot session - extensions are classic NSExtension app extensions (the ExtensionKit product type expects an @main entry and made the app fail to install); shield-store tracking moved to app-group defaults so the app and extensions see one consistent set - maps iOS 26's new .approvedWithDataAccess authorization status to approved (it previously fell through @unknown default to notDetermined) - shares the OpenAppLock scheme (Xcode dropped the autocreated one when targets were added)
76 lines
2.3 KiB
Swift
76 lines
2.3 KiB
Swift
//
|
|
// RuleSnapshot.swift
|
|
// OpenAppLock
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Codable mirror of a rule, written to the app group by the app whenever
|
|
/// rules change so the Screen Time extensions (which cannot open the
|
|
/// SwiftData store) know what to enforce.
|
|
struct RuleSnapshot: Codable, Equatable {
|
|
var id: UUID
|
|
var name: String
|
|
var kindRaw: String
|
|
var isEnabled: Bool
|
|
var hardMode: Bool
|
|
var blockAdultContent: Bool
|
|
var selectionModeRaw: String
|
|
var selectionData: Data?
|
|
var dayNumbers: [Int]
|
|
var dailyLimitMinutes: Int
|
|
var maxOpens: Int
|
|
var pausedUntil: Date?
|
|
|
|
var kind: RuleKind { RuleKind(rawValue: kindRaw) ?? .schedule }
|
|
var days: Set<Weekday> { Set(dayNumbers.compactMap(Weekday.init(rawValue:))) }
|
|
|
|
func isScheduledToday(at now: Date, calendar: Calendar = .current) -> Bool {
|
|
guard let weekday = Weekday(rawValue: calendar.component(.weekday, from: now)) else {
|
|
return false
|
|
}
|
|
return days.contains(weekday)
|
|
}
|
|
|
|
/// Whether the given usage exhausts this rule's daily budget.
|
|
func limitReached(given usage: RuleUsage) -> Bool {
|
|
switch kind {
|
|
case .schedule: false
|
|
case .timeLimit: usage.minutesUsed >= dailyLimitMinutes
|
|
case .openLimit: usage.opensUsed >= maxOpens
|
|
}
|
|
}
|
|
|
|
/// Whether the user unblocked this rule for the rest of the day.
|
|
func isPaused(at now: Date) -> Bool {
|
|
guard let pausedUntil else { return false }
|
|
return pausedUntil > now
|
|
}
|
|
}
|
|
|
|
/// Persistence for the rule mirror in the shared app-group defaults.
|
|
final class RuleSnapshotStore {
|
|
private static let key = "ruleSnapshots"
|
|
private let defaults: UserDefaults
|
|
|
|
init(defaults: UserDefaults = AppGroup.defaults) {
|
|
self.defaults = defaults
|
|
}
|
|
|
|
func save(_ snapshots: [RuleSnapshot]) {
|
|
guard let data = try? JSONEncoder().encode(snapshots) else { return }
|
|
defaults.set(data, forKey: Self.key)
|
|
}
|
|
|
|
func load() -> [RuleSnapshot] {
|
|
guard let data = defaults.data(forKey: Self.key),
|
|
let snapshots = try? JSONDecoder().decode([RuleSnapshot].self, from: data)
|
|
else { return [] }
|
|
return snapshots
|
|
}
|
|
|
|
func snapshot(for ruleID: UUID) -> RuleSnapshot? {
|
|
load().first { $0.id == ruleID }
|
|
}
|
|
}
|