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. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
2.4 KiB
Swift
69 lines
2.4 KiB
Swift
//
|
|
// MainTabView.swift
|
|
// OpenAppLock
|
|
//
|
|
|
|
import SwiftData
|
|
import SwiftUI
|
|
|
|
/// The post-onboarding shell: a three-tab layout (Home / Rules / Settings).
|
|
///
|
|
/// The app-level enforcement lifecycle lives here, not on any one tab, so it
|
|
/// runs regardless of the selected tab: a 30 s `refresh` loop, a reconcile on
|
|
/// any blocking-relevant rule change, and a reconcile whenever the app becomes
|
|
/// active (so Uninstall Protection re-evaluates on every foreground).
|
|
struct MainTabView: View {
|
|
@Environment(\.modelContext) private var modelContext
|
|
@Environment(RuleEnforcer.self) private var enforcer
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
@Query(sort: \BlockingRule.createdAt) private var rules: [BlockingRule]
|
|
|
|
var body: some View {
|
|
TabView {
|
|
HomeView()
|
|
.tabItem { Label("Home", systemImage: "house") }
|
|
RulesListView()
|
|
.tabItem { Label("Rules", systemImage: "shield.lefthalf.filled") }
|
|
SettingsView()
|
|
.tabItem { Label("Settings", systemImage: "gearshape") }
|
|
}
|
|
.task {
|
|
await enforcementLoop()
|
|
}
|
|
.onChange(of: ruleChangeToken) {
|
|
refreshEnforcement()
|
|
}
|
|
.onChange(of: scenePhase) { _, phase in
|
|
if phase == .active { refreshEnforcement() }
|
|
}
|
|
}
|
|
|
|
// MARK: - Enforcement
|
|
|
|
/// Changes whenever any rule's blocking-relevant state changes.
|
|
private var ruleChangeToken: String {
|
|
rules.map {
|
|
"\($0.id)|\($0.isEnabled)|\($0.hardMode)|\($0.blockAdultContent)|"
|
|
+ "\($0.startMinutes)|\($0.endMinutes)|\($0.dayNumbers)|"
|
|
+ "\($0.selectionModeRaw)|\($0.appList?.id.uuidString ?? "-")|"
|
|
+ "\($0.appList?.selectionCount ?? 0)|"
|
|
+ "\($0.pausedUntil?.timeIntervalSince1970 ?? 0)"
|
|
}
|
|
.joined(separator: ",")
|
|
}
|
|
|
|
private func refreshEnforcement() {
|
|
enforcer.refresh(rules: rules)
|
|
}
|
|
|
|
/// Keeps shields in sync while the app is open, so windows that begin or
|
|
/// end while the user is looking at the screen take effect promptly.
|
|
private func enforcementLoop() async {
|
|
while !Task.isCancelled {
|
|
let allRules = (try? modelContext.fetch(FetchDescriptor<BlockingRule>())) ?? []
|
|
enforcer.refresh(rules: allRules)
|
|
try? await Task.sleep(for: .seconds(30))
|
|
}
|
|
}
|
|
}
|