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:
2026-06-13 22:17:08 -04:00
parent 0a0d00f53a
commit a658d196c4
26 changed files with 1039 additions and 427 deletions

View File

@@ -12,6 +12,7 @@ final class RuleCreationUITests: XCTestCase {
func testCreateScheduleRuleFromTypeCard() throws {
let app = XCUIApplication.launchOpenAppLock()
app.goToRulesTab()
app.element("emptyRulesCard").waitToAppear()
app.buttons["newRuleButton"].tap()
@@ -33,6 +34,7 @@ final class RuleCreationUITests: XCTestCase {
func testCreateRuleFromPreset() throws {
let app = XCUIApplication.launchOpenAppLock()
app.goToRulesTab()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["preset-work-time"].waitToAppear().tap()
@@ -44,6 +46,7 @@ final class RuleCreationUITests: XCTestCase {
func testRenameRuleInEditor() throws {
let app = XCUIApplication.launchOpenAppLock()
app.goToRulesTab()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -63,6 +66,7 @@ final class RuleCreationUITests: XCTestCase {
func testDayTogglesFillRowAndHaveLargeTapTargets() throws {
let app = XCUIApplication.launchOpenAppLock()
app.goToRulesTab()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -81,6 +85,7 @@ final class RuleCreationUITests: XCTestCase {
func testDayTogglesUpdateSummary() throws {
let app = XCUIApplication.launchOpenAppLock()
app.goToRulesTab()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -93,6 +98,7 @@ final class RuleCreationUITests: XCTestCase {
func testCreateTimeLimitRule() throws {
let app = XCUIApplication.launchOpenAppLock()
app.goToRulesTab()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-timeLimit"].waitToAppear().tap()
@@ -109,6 +115,7 @@ final class RuleCreationUITests: XCTestCase {
func testAdultContentToggleFlowsToDetail() throws {
let app = XCUIApplication.launchOpenAppLock()
app.goToRulesTab()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -126,6 +133,7 @@ final class RuleCreationUITests: XCTestCase {
func testAdultContentDefaultsToAllowed() throws {
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.goToRulesTab()
app.buttons["ruleCard-Work Time"].waitToAppear().tap()
let row = app.element("detailRow-Adult websites").waitToAppear()
XCTAssertTrue(row.label.contains("Allowed"), "Got: \(row.label)")
@@ -135,6 +143,7 @@ final class RuleCreationUITests: XCTestCase {
/// not offer the toggle, and the rule's detail must not show the row.
func testTimeLimitOmitsAdultContent() throws {
let app = XCUIApplication.launchOpenAppLock()
app.goToRulesTab()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-timeLimit"].waitToAppear().tap()
@@ -159,6 +168,7 @@ final class RuleCreationUITests: XCTestCase {
func testEditorSupportsNativeSwipeBack() throws {
let app = XCUIApplication.launchOpenAppLock()
app.goToRulesTab()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
app.staticTexts["ruleEditorTitle"].waitToAppear()
@@ -174,6 +184,7 @@ final class RuleCreationUITests: XCTestCase {
func testNewRuleSheetShowsTypesAndPresets() throws {
let app = XCUIApplication.launchOpenAppLock()
app.goToRulesTab()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear()