Replace the custom themed presentation with the bare native iOS design
language across a three-tab TabView (Home / Rules / Settings):
- MainTabView hosts the enforcement lifecycle (refresh loop, rule-change
and scene-active reconcile) so it runs regardless of the selected tab
- HomeView: "Currently Blocking" + "Usage" sections (replaces AppsHomeView)
- RulesListView: rules grouped into Schedule / Time Limit / Open Limit
- SettingsView: Uninstall Protection toggle + Manage App Lists
- AppListLibraryView / ManageAppListsView: shared App List management
- AppSettings: app-group-backed settings store
Uninstall Protection: while enabled and any Hard Mode rule is actively
blocking, deny device app-removal via a dedicated ManagedSettingsStore
(RulePolicy.shouldDenyAppRemoval -> RuleEnforcer -> ShieldController).
Slim AppListPickerSheet to the picker role; drop AppsHomeView and the
custom Theme/HoldToCommitButton/RuleCardView chrome. Update spec section 6
and the unit/UI test suites accordingly.
RuleEnforcer.refresh only shielded a limit rule once its budget was
spent, so it tore down the background's proactive open-limit "turnstile"
shield (the shield is how opens are counted) whenever the app was
foregrounded, and a freshly created open-limit rule did nothing until
the next midnight reset.
refresh now applies the same proactive gate as
LimitEnforcement.handleDayStart: an enabled, scheduled-today, un-paused
open-limit rule is shielded even before its budget is spent, so it takes
effect immediately. A new app-group OpenSessionStore records a granted
"Open" session's expiry so neither enforcement path re-shields (and cuts
short) a sanctioned ~15-minute session.
Overlapping rules already enforce strictly: each rule shields its own
ManagedSettingsStore and Screen Time unions them, so an app is blocked
if any covering rule blocks it. Document that model in the spec (§4.8)
and lock it in with tests, including a time limit blocking during an
open-limit's granted session and a daily opens reset.
Co-Authored-By: Claude <noreply@anthropic.com>
Replace the wide BlockingRule/RuleDraft "god struct" — where every option
existed for every kind — with a RuleConfiguration sum type that carries only
the options each kind actually has:
.schedule(ScheduleConfig: window + selectionMode + blockAdultContent)
.timeLimit(TimeLimitConfig: dailyLimitMinutes)
.openLimit(OpenLimitConfig: maxOpens)
Name, days, Hard Mode, app list, and pause stay common to all kinds. This
makes illegal states unrepresentable: Block / Allow Only and Block Adult
Content are now structurally Schedule-only.
User-visible behavior change: the Time Limit and Open Limit editors no longer
offer a Block Adult Content toggle, and their detail sheets drop the "Adult
websites" row — those never made sense for a usage budget. Limit rules are
always Block and never engage the web-content filter.
BlockingRule keeps flat columns as raw persistence behind a computed
`configuration` bridge (lowest SwiftData risk; the cross-process RuleSnapshot
wire format and the Screen Time extensions are untouched). Logic, the editors,
and the detail sheet all switch on the sum type.
Spec (§1, §3.5/§3.6, §5.2) updated first; tests reworked to the new API with
new structural-guarantee unit tests and a UI test asserting the Time Limit
editor/detail omit adult content. Full suite green (180 tests).
Co-Authored-By: Claude <noreply@anthropic.com>
Full rename ahead of App Store submission: project, targets, bundle
identifiers (dev.bchen.Severed -> dev.bchen.OpenAppLock), entitlements
file, app entry point, user-facing onboarding strings, test helpers,
and docs. Verified: all 62 unit tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>