- Add Block Adult Content toggle to all three rule editors, persisted as BlockingRule.blockAdultContent (inline default for clean migration of existing stores) and surfaced in the detail sheet as an 'Adult websites: Blocked/Allowed' row - Engage Screen Time's adult-website filter (webContent.blockedByFilter = .auto()) alongside the rule's shield and clear it when the shield clears - Replace the New Rule sheet's view-swap with a NavigationStack push (navigationDestination(item:)); the editor uses native chrome there (system back button, inline title, toolbar rename), enabling the push animation and edge-swipe back - Tests: 93 passing (+2 unit: draft round-trip and enforcer forwarding; +3 UI: toggle-to-detail flow, default-allowed row, swipe-back as a behavioral proof of native navigation) - Spec updated accordingly (editor sections, behavior, data model, navigation note)
47 lines
1.5 KiB
Swift
47 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,
|
|
blockAdultContent: rule.blockAdultContent
|
|
)
|
|
}
|
|
shields.clearShields(except: active)
|
|
blockingRuleIDs = active
|
|
}
|
|
}
|