Rebuild the app around recurring screen-time blocking rules modeled on Opal's My Apps tab: - Onboarding with FamilyControls Screen Time authorization - Apps home with Blocked Apps tiles and live rule cards - New Rule sheet: Schedule/Time Limit/Open Limit types + preset gallery - Rule editors: time windows (incl. overnight), day picker, app selection via FamilyActivityPicker (Block/Allow Only), rename, Hold to Commit - Hard Mode: active hard rules cannot be edited, disabled, deleted, or unblocked until their window ends; soft rules pause until next window - Shield enforcement through per-rule ManagedSettingsStore + RuleEnforcer - 73 unit tests (Swift Testing) + 16 UI tests (XCUITest) with launch-arg harness for in-memory storage, mocked authorization, seeded scenarios - docs/RULES_FEATURE_SPEC.md: spec derived from the reference recording Known gap: background window transitions and time/open-limit thresholds need a DeviceActivityMonitor extension; shields currently sync while the app is running. Co-Authored-By: Claude <noreply@anthropic.com>
44 lines
1.5 KiB
Swift
44 lines
1.5 KiB
Swift
//
|
|
// RuleEnforcer.swift
|
|
// Severed
|
|
//
|
|
|
|
import Foundation
|
|
import Observation
|
|
|
|
/// Turns the current set of rules into shield state: schedule rules with an
|
|
/// active, un-paused window are shielded; everything else is cleared.
|
|
///
|
|
/// Time-limit and open-limit rules need a DeviceActivity monitor extension to
|
|
/// react to usage thresholds in the background; they are persisted and shown
|
|
/// in the UI but not yet enforced here.
|
|
@Observable
|
|
final class RuleEnforcer {
|
|
private(set) var blockingRuleIDs: Set<UUID> = []
|
|
private let shields: ShieldApplying
|
|
|
|
init(shields: ShieldApplying) {
|
|
self.shields = shields
|
|
}
|
|
|
|
/// Recomputes shields from scratch. Call on launch, on any rule change,
|
|
/// and periodically while the app is visible. Also expires stale pauses.
|
|
func refresh(rules: [BlockingRule], at now: Date = .now, calendar: Calendar = .current) {
|
|
var active: Set<UUID> = []
|
|
for rule in rules {
|
|
if let pausedUntil = rule.pausedUntil, pausedUntil <= now {
|
|
rule.pausedUntil = nil
|
|
}
|
|
guard rule.kind == .schedule,
|
|
rule.status(at: now, calendar: calendar).isActive
|
|
else { continue }
|
|
active.insert(rule.id)
|
|
shields.applyShield(
|
|
ruleID: rule.id, selectionData: rule.selectionData, mode: rule.selectionMode
|
|
)
|
|
}
|
|
shields.clearShields(except: active)
|
|
blockingRuleIDs = active
|
|
}
|
|
}
|