feat: adult content filter toggle + native New Rule navigation

- 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)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:34:52 -04:00
parent e6c87baeba
commit c99538565c
11 changed files with 182 additions and 26 deletions

View File

@@ -19,6 +19,9 @@ final class BlockingRule {
var isEnabled: Bool
/// Hard block: while the rule is active it cannot be disabled, edited, or unblocked.
var hardMode: Bool
/// Engage Screen Time's adult-website filter while this rule is blocking.
/// Inline default so existing stores migrate cleanly.
var blockAdultContent: Bool = false
var selectionModeRaw: String
/// Encoded `FamilyActivitySelection` (opaque tokens). Nil until the user picks apps.
var selectionData: Data?
@@ -42,6 +45,7 @@ final class BlockingRule {
kind: RuleKind = .schedule,
isEnabled: Bool = true,
hardMode: Bool = false,
blockAdultContent: Bool = false,
selectionMode: SelectionMode = .block,
selectionData: Data? = nil,
selectionCount: Int = 0,
@@ -58,6 +62,7 @@ final class BlockingRule {
self.kindRaw = kind.rawValue
self.isEnabled = isEnabled
self.hardMode = hardMode
self.blockAdultContent = blockAdultContent
self.selectionModeRaw = selectionMode.rawValue
self.selectionData = selectionData
self.selectionCount = selectionCount

View File

@@ -6,8 +6,9 @@
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 {
/// edit never touches the persisted model. Hashable so it can drive
/// `navigationDestination(item:)`.
struct RuleDraft: Hashable {
var name: String
var kind: RuleKind
var days: Set<Weekday>
@@ -16,6 +17,7 @@ struct RuleDraft: Equatable {
var dailyLimitMinutes: Int
var maxOpens: Int
var hardMode: Bool
var blockAdultContent: Bool
var selectionMode: SelectionMode
var selectionData: Data?
var selectionCount: Int
@@ -31,6 +33,7 @@ struct RuleDraft: Equatable {
self.dailyLimitMinutes = 45
self.maxOpens = 5
self.hardMode = false
self.blockAdultContent = false
self.selectionMode = .block
self.selectionData = nil
self.selectionCount = 0
@@ -45,6 +48,7 @@ struct RuleDraft: Equatable {
self.dailyLimitMinutes = rule.dailyLimitMinutes
self.maxOpens = rule.maxOpens
self.hardMode = rule.hardMode
self.blockAdultContent = rule.blockAdultContent
self.selectionMode = rule.selectionMode
self.selectionData = rule.selectionData
self.selectionCount = rule.selectionCount
@@ -67,6 +71,7 @@ struct RuleDraft: Equatable {
rule.dailyLimitMinutes = dailyLimitMinutes
rule.maxOpens = maxOpens
rule.hardMode = hardMode
rule.blockAdultContent = blockAdultContent
rule.selectionMode = selectionMode
rule.selectionData = selectionData
rule.selectionCount = selectionCount