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>
66 lines
2.4 KiB
Swift
66 lines
2.4 KiB
Swift
//
|
|
// SampleRules.swift
|
|
// Severed
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftData
|
|
|
|
/// Builds deterministic rules for UI-test scenarios, positioned relative to
|
|
/// "now" so an active window is genuinely active whenever the test runs.
|
|
enum SampleRules {
|
|
static func seed(
|
|
_ scenario: LaunchConfiguration.SeedScenario,
|
|
into context: ModelContext,
|
|
now: Date = .now,
|
|
calendar: Calendar = .current
|
|
) {
|
|
switch scenario {
|
|
case .standard:
|
|
context.insert(activeRule(named: "Work Time", hardMode: false, now: now, calendar: calendar))
|
|
context.insert(upcomingRule(named: "Sleep", now: now, calendar: calendar))
|
|
case .hardModeActive:
|
|
context.insert(activeRule(named: "Locked In", hardMode: true, now: now, calendar: calendar))
|
|
context.insert(upcomingRule(named: "Sleep", now: now, calendar: calendar))
|
|
}
|
|
}
|
|
|
|
/// A schedule rule whose window started up to an hour ago and runs for
|
|
/// several hours, clamped so it never crosses midnight accidentally.
|
|
static func activeRule(
|
|
named name: String, hardMode: Bool, now: Date, calendar: Calendar = .current
|
|
) -> BlockingRule {
|
|
let nowMinutes = minutesIntoDay(of: now, calendar: calendar)
|
|
let start = max(0, nowMinutes - 60)
|
|
let end = min(24 * 60 - 1, nowMinutes + 6 * 60)
|
|
return BlockingRule(
|
|
name: name,
|
|
hardMode: hardMode,
|
|
days: Weekday.everyDay,
|
|
startMinutes: start,
|
|
endMinutes: end
|
|
)
|
|
}
|
|
|
|
/// A schedule rule starting a couple of hours from now (possibly wrapping
|
|
/// past midnight, which simply makes it start tomorrow).
|
|
static func upcomingRule(
|
|
named name: String, now: Date, calendar: Calendar = .current
|
|
) -> BlockingRule {
|
|
let nowMinutes = minutesIntoDay(of: now, calendar: calendar)
|
|
let start = (nowMinutes + 2 * 60) % (24 * 60)
|
|
let end = (start + 8 * 60) % (24 * 60)
|
|
return BlockingRule(
|
|
name: name,
|
|
days: Weekday.everyDay,
|
|
startMinutes: start,
|
|
endMinutes: end
|
|
)
|
|
}
|
|
|
|
private static func minutesIntoDay(of date: Date, calendar: Calendar) -> Int {
|
|
let components = calendar.dateComponents([.hour, .minute], from: date)
|
|
return (components.hour ?? 0) * 60 + (components.minute ?? 0)
|
|
}
|
|
}
|