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.
55 lines
1.7 KiB
Swift
55 lines
1.7 KiB
Swift
//
|
|
// HoldToCommitButton.swift
|
|
// Severed
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
/// The deliberate-friction commit button: the user must press and hold for a
|
|
/// second to save a rule, mirroring the reference app's "Hold to Commit".
|
|
struct HoldToCommitButton: View {
|
|
var title = "Hold to Commit"
|
|
var holdDuration: Double = 1.0
|
|
let action: () -> Void
|
|
|
|
@State private var progress: Double = 0
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 28)
|
|
.fill(Theme.surfaceElevated)
|
|
GeometryReader { proxy in
|
|
RoundedRectangle(cornerRadius: 28)
|
|
.fill(Theme.commitGradient)
|
|
.opacity(0.85)
|
|
.frame(width: proxy.size.width * progress)
|
|
.animation(.linear(duration: 0.1), value: progress == 0)
|
|
}
|
|
Text(title)
|
|
.font(.system(size: 17, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
}
|
|
.frame(height: 56)
|
|
.clipShape(RoundedRectangle(cornerRadius: 28))
|
|
.contentShape(RoundedRectangle(cornerRadius: 28))
|
|
.onLongPressGesture(minimumDuration: holdDuration) {
|
|
progress = 0
|
|
action()
|
|
} onPressingChanged: { pressing in
|
|
withAnimation(.linear(duration: pressing ? holdDuration : 0.2)) {
|
|
progress = pressing ? 1 : 0
|
|
}
|
|
}
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityAddTraits(.isButton)
|
|
.accessibilityIdentifier("holdToCommitButton")
|
|
.accessibilityLabel(title)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
HoldToCommitButton { }
|
|
.padding()
|
|
.background(Theme.background)
|
|
}
|