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>
85 lines
2.4 KiB
Swift
85 lines
2.4 KiB
Swift
//
|
||
// RuleDraft.swift
|
||
// Severed
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// Value-type working copy of a rule used by the editors, so cancelling an
|
||
/// edit never touches the persisted model.
|
||
struct RuleDraft: Equatable {
|
||
var name: String
|
||
var kind: RuleKind
|
||
var days: Set<Weekday>
|
||
var startMinutes: Int
|
||
var endMinutes: Int
|
||
var dailyLimitMinutes: Int
|
||
var maxOpens: Int
|
||
var hardMode: Bool
|
||
var selectionMode: SelectionMode
|
||
var selectionData: Data?
|
||
var selectionCount: Int
|
||
|
||
/// A fresh draft for a new rule of the given kind, using the reference
|
||
/// app's defaults (9–5 weekdays schedule, 45m/day, 5 opens/day).
|
||
init(kind: RuleKind) {
|
||
self.name = kind.defaultRuleName
|
||
self.kind = kind
|
||
self.days = Weekday.weekdays
|
||
self.startMinutes = 9 * 60
|
||
self.endMinutes = 17 * 60
|
||
self.dailyLimitMinutes = 45
|
||
self.maxOpens = 5
|
||
self.hardMode = false
|
||
self.selectionMode = .block
|
||
self.selectionData = nil
|
||
self.selectionCount = 0
|
||
}
|
||
|
||
init(rule: BlockingRule) {
|
||
self.name = rule.name
|
||
self.kind = rule.kind
|
||
self.days = rule.days
|
||
self.startMinutes = rule.startMinutes
|
||
self.endMinutes = rule.endMinutes
|
||
self.dailyLimitMinutes = rule.dailyLimitMinutes
|
||
self.maxOpens = rule.maxOpens
|
||
self.hardMode = rule.hardMode
|
||
self.selectionMode = rule.selectionMode
|
||
self.selectionData = rule.selectionData
|
||
self.selectionCount = rule.selectionCount
|
||
}
|
||
|
||
init(preset: RulePreset) {
|
||
self.init(kind: .schedule)
|
||
self.name = preset.name
|
||
self.days = preset.days
|
||
self.startMinutes = preset.startMinutes
|
||
self.endMinutes = preset.endMinutes
|
||
}
|
||
|
||
func apply(to rule: BlockingRule) {
|
||
rule.name = name
|
||
rule.kind = kind
|
||
rule.days = days
|
||
rule.startMinutes = startMinutes
|
||
rule.endMinutes = endMinutes
|
||
rule.dailyLimitMinutes = dailyLimitMinutes
|
||
rule.maxOpens = maxOpens
|
||
rule.hardMode = hardMode
|
||
rule.selectionMode = selectionMode
|
||
rule.selectionData = selectionData
|
||
rule.selectionCount = selectionCount
|
||
}
|
||
|
||
func makeRule() -> BlockingRule {
|
||
let rule = BlockingRule(name: name, kind: kind)
|
||
apply(to: rule)
|
||
return rule
|
||
}
|
||
|
||
var schedule: RuleSchedule {
|
||
RuleSchedule(startMinutes: startMinutes, endMinutes: endMinutes, days: days)
|
||
}
|
||
}
|