feat: clone Opal's rules feature with hard block enforcement
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>
This commit is contained in:
43
Severed/Services/RuleEnforcer.swift
Normal file
43
Severed/Services/RuleEnforcer.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user