feat: re-skin to native three-tab UI and add Uninstall Protection
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.
This commit is contained in:
@@ -91,14 +91,16 @@ Dark theme throughout (near-black background, very dark green tint).
|
||||
*(OpenAppLock)* Time/Open Limit rules whose budget is spent for the day
|
||||
also appear here, blocked until midnight.
|
||||
3. **Usage** *(OpenAppLock addition — not in the Opal video)* — a section
|
||||
directly below Blocked Apps showing live tracking for every enabled
|
||||
Time/Open Limit rule scheduled today:
|
||||
- Time Limit row: subtitle "18m of 45m used today", trailing "27m left";
|
||||
when spent: "Blocked until tomorrow" (red).
|
||||
- Open Limit row: subtitle "2 of 5 opens today", trailing "3 opens left";
|
||||
when spent: "Blocked until tomorrow" (red).
|
||||
Usage numbers come from the shared app-group ledger written by the
|
||||
DeviceActivity monitor and shield-action extensions.
|
||||
showing live tracking for every enabled Time/Open Limit rule scheduled today
|
||||
**that is not currently blocking**. Each row leads its subtitle with the rule
|
||||
**type** so the kind is clear without relying on the icon:
|
||||
- Time Limit row: subtitle "Time Limit · 18m of 45m used today", trailing "27m left".
|
||||
- Open Limit row: subtitle "Open Limit · 2 of 5 opens today", trailing "3 opens left".
|
||||
A rule whose budget is **spent** (actively blocking) **moves out of Usage into
|
||||
the "Currently Blocking" section** (it shows its type + usage there instead);
|
||||
a *soft-unblocked* spent rule is paused (not blocking), so it returns to Usage
|
||||
reading "Unblocked until tomorrow". Usage numbers come from the shared app-group
|
||||
ledger written by the DeviceActivity monitor and shield-action extensions.
|
||||
3. **Rules** — header row: "Rules ›" (leading, tappable to a full list,
|
||||
presumably) and "**+ New**" (trailing, green tint) which opens the New Rule
|
||||
sheet.
|
||||
@@ -514,11 +516,28 @@ Xh", "Xh left") is **derived**, not stored.
|
||||
OpenAppLock has since replaced the Opal-style custom presentation with the bare
|
||||
iOS design language, keeping the backend (models, logic, services), the
|
||||
flows, and the accessibility identifiers intact. Sections 1–5 remain as the
|
||||
reference for *what* the feature does; presentation now maps as follows:
|
||||
reference for *what* the feature does; presentation now maps as follows.
|
||||
|
||||
After onboarding the app is a three-tab `TabView` (`MainTabView`), each tab its
|
||||
own `NavigationStack`:
|
||||
|
||||
```
|
||||
TabView: [Home] [Rules] [Settings]
|
||||
│ │ └── "Uninstall Protection" toggle + "Manage App Lists" ─▶ App List library (management mode)
|
||||
│ └── rules grouped into Schedule / Time Limit / Open Limit sections; "+" ─▶ New Rule sheet
|
||||
│ └── tap a rule row ─▶ Rule Detail sheet ─▶ "Edit Rule" ─▶ Rule Editor
|
||||
└── "Currently Blocking" section + "Usage" section
|
||||
```
|
||||
|
||||
The app-level **enforcement lifecycle** (the `enforcer.refresh` 30 s loop, the
|
||||
rule-change reconcile, and a scene-active reconcile) lives on `MainTabView`, so
|
||||
it runs regardless of the selected tab.
|
||||
|
||||
| Spec element | Native presentation |
|
||||
|---|---|
|
||||
| Apps home | `NavigationStack` + `List`; "Blocked Apps" and "Rules" sections; **rules are list rows** (kind icon, name, block summary, trailing live status — green when active); "+" toolbar button |
|
||||
| Home tab | `NavigationStack` + `List`. **"Currently Blocking"** section (renamed from "Blocked Apps") — the *rules* blocking right now: **no leading icon**; a Hard Mode rule shows a trailing `lock.fill` (the block can't be lifted), a soft rule shows a trailing "Unblock" button; tapping a hard row shows the "Hard Mode is on" alert, a soft row the unblock dialog. A limit rule whose budget is **spent** appears here (moved out of Usage) with a `<Type> · <usage>` subtitle. **"Usage"** section: every enabled limit rule scheduled today that is *not* currently blocking, each row a `<Type> · NN of MM used today` subtitle + trailing remaining/blocked label. |
|
||||
| Rules tab | `NavigationStack` + `List` split into **Schedule / Time Limit / Open Limit** sections (empty sections hidden); **rules are list rows** (leading kind icon, name, block summary, trailing live status — green when active); "+" toolbar button opens the New Rule sheet; tapping a row opens the Rule Detail sheet. |
|
||||
| Settings tab | `NavigationStack` + `Form`. **Uninstall Protection** toggle — while on, the device's app-removal is denied (`ManagedSettingsStore.application.denyAppRemoval`) whenever any Hard Mode rule is actively blocking. **Manage App Lists** pushes the shared App List library in management mode (create / edit / delete, honoring the Hard Mode lock — same flow as the rule editor's picker, minus selection). |
|
||||
| Rule detail | Sheet with inline nav title (name + "Schedule, 6h left" caption), `LabeledContent` rows, "Edit Rule" row pushes the editor; hard-locked rules show a lock row instead |
|
||||
| New Rule | `List` with a "Rule Type" section and preset sections as plain rows; editor pushed via `navigationDestination(item:)` |
|
||||
| Rule editor | Native `Form`: an inline **Name text field** at the top (no separate rename button; empty names fall back to the kind default), `DatePicker` rows, full-width day-circle row (≥44pt tap targets) with the summary in the section header, toggle rows with footers, stepper rows. Both modes commit via a **checkmark** in the navigation bar (labels: "Add Rule" / "Done"; replaces Hold to Commit). In edit mode an **ellipsis menu** ("Rule Actions") next to the checkmark holds Disable Rule and the destructive Delete Rule |
|
||||
@@ -526,3 +545,22 @@ reference for *what* the feature does; presentation now maps as follows:
|
||||
|
||||
Dropped custom components: `Theme`, `HoldToCommitButton`, `RuleCardView`,
|
||||
icon-pair/circle-button chrome.
|
||||
|
||||
### 6.1 Uninstall Protection (Settings)
|
||||
|
||||
A device-wide opt-in that makes Hard Mode harder to escape: while it is on **and**
|
||||
any Hard Mode rule is actively blocking, the user cannot delete apps from the
|
||||
device. `RulePolicy.shouldDenyAppRemoval(rules:enabled:usageFor:)` (= setting on
|
||||
AND any rule `isHardLocked`) is the single gate; `RuleEnforcer.refresh` applies it
|
||||
through `ShieldApplying.setAppRemovalDenied`, which sets
|
||||
`ManagedSettingsStore(named: "uninstall-protection").application.denyAppRemoval`
|
||||
(`true` to engage, `nil` to relinquish) on a **dedicated** store so per-rule
|
||||
shield clears never touch it. The setting persists in the app-group defaults
|
||||
(`uninstallProtectionEnabled`).
|
||||
|
||||
Enforced on the **foreground path only** for v1 (launch + 30 s loop + rule change
|
||||
+ scene-active). Known limitation: a Hard Mode window that *ends* while the app is
|
||||
closed leaves protection engaged until the app is next foregrounded — the safe
|
||||
failure direction for a locker. Background recompute in the monitor extension is a
|
||||
follow-up. Like all Screen Time behavior, the real device effect is only
|
||||
observable on a device (the simulator uses mock shields).
|
||||
|
||||
Reference in New Issue
Block a user