diff options
| author | Christian Cleberg <[email protected]> | 2026-07-17 08:36:05 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-17 10:12:36 -0500 |
| commit | b4b94d54cbed9334a28cdcd70d3a8a0fd06e04ef (patch) | |
| tree | 8fa4b408729da32ec2c547081ec2bba13b893fa2 /DomainDig/RootTabView.swift | |
| parent | 065e1e77ff00e8f9c8785393b5eba6c3e0bb2bb1 (diff) | |
| download | domain-dig-b4b94d54cbed9334a28cdcd70d3a8a0fd06e04ef.tar.gz domain-dig-b4b94d54cbed9334a28cdcd70d3a8a0fd06e04ef.tar.bz2 domain-dig-b4b94d54cbed9334a28cdcd70d3a8a0fd06e04ef.zip | |
Implement v4.6.0: sweep Live Activity, share extension, iPad split view, actionable notifications
- Sweep Live Activity: SweepActivityAttributes (Shared/), lock-screen + Dynamic
Island UI in the widget extension, driven by SweepActivityController wired into
the batch pipeline (begin/update/end); NSSupportsLiveActivities in Info.plist.
- Share extension (DomainDigShareExtension): accepts a web URL from the share
sheet, extracts the host, and hands it to the app via the App Group inbox
(DomainDigShareInbox); the app consumes it on activation and inspects it.
- iPad layout: RootTabView uses NavigationSplitView in the regular size class
and the tab bar in compact.
- Actionable notifications: per-domain threadIdentifier grouping, a Re-inspect
action, and tap routing into the domain detail via the intent router.
- Bump version to 4.6.0 (build 38) across app, widget, and share targets.
Diffstat (limited to 'DomainDig/RootTabView.swift')
| -rw-r--r-- | DomainDig/RootTabView.swift | 136 |
1 files changed, 93 insertions, 43 deletions
diff --git a/DomainDig/RootTabView.swift b/DomainDig/RootTabView.swift index 9823d37..db1f524 100644 --- a/DomainDig/RootTabView.swift +++ b/DomainDig/RootTabView.swift @@ -1,15 +1,36 @@ import SwiftUI -private enum RootTab: Hashable { +private enum RootTab: Hashable, CaseIterable { case dashboard case audit case history case inspect case settings + + var title: String { + switch self { + case .dashboard: return "Dashboard" + case .audit: return "Audit" + case .history: return "History" + case .inspect: return "Inspect" + case .settings: return "Settings" + } + } + + var systemImage: String { + switch self { + case .dashboard: return "square.grid.2x2" + case .audit: return "checklist" + case .history: return "clock.arrow.trianglehead.counterclockwise.rotate.90" + case .inspect: return "magnifyingglass" + case .settings: return "gearshape" + } + } } struct RootTabView: View { @Bindable var viewModel: DomainViewModel + @Environment(\.horizontalSizeClass) private var horizontalSizeClass @State private var purchaseService = PurchaseService.shared @State private var intentRouter = DomainDigIntentRouter.shared @State private var detailDomain: TrackedDomain? @@ -18,44 +39,12 @@ struct RootTabView: View { var body: some View { let _ = purchaseService.currentTier - TabView(selection: $selectedTab) { - NavigationStack { - DashboardView(viewModel: viewModel) - } - .tabItem { - Label("Dashboard", systemImage: "square.grid.2x2") - } - .tag(RootTab.dashboard) - - NavigationStack { - AuditListView(viewModel: viewModel) - } - .tabItem { - Label("Audit", systemImage: "checklist") - } - .tag(RootTab.audit) - - NavigationStack { - HistoryView(viewModel: viewModel) - } - .tabItem { - Label("History", systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90") - } - .tag(RootTab.history) - - ContentView(viewModel: viewModel) - .tabItem { - Label("Inspect", systemImage: "magnifyingglass") - } - .tag(RootTab.inspect) - - NavigationStack { - SettingsView(viewModel: viewModel) - } - .tabItem { - Label("Settings", systemImage: "gearshape") + Group { + if horizontalSizeClass == .regular { + splitLayout + } else { + tabLayout } - .tag(RootTab.settings) } .sheet(isPresented: Binding( get: { viewModel.isPaywallPresented }, @@ -79,16 +68,16 @@ struct RootTabView: View { } ) } - .onChange(of: purchaseService.currentTier) { _, newValue in - if newValue != .free, selectedTab == .inspect, viewModel.trackedDomains.isEmpty == false { - selectedTab = .dashboard - } - } .sheet(item: $detailDomain) { trackedDomain in NavigationStack { TrackedDomainDetailView(viewModel: viewModel, trackedDomain: trackedDomain) } } + .onChange(of: purchaseService.currentTier) { _, newValue in + if newValue != .free, selectedTab == .inspect, viewModel.trackedDomains.isEmpty == false { + selectedTab = .dashboard + } + } .onOpenURL { url in guard let action = DomainDigDeepLink.action(from: url) else { return } perform(action) @@ -101,6 +90,67 @@ struct RootTabView: View { } } + // MARK: Layouts + + /// Compact (iPhone, iPad slide-over): the classic tab bar. + private var tabLayout: some View { + TabView(selection: $selectedTab) { + ForEach(RootTab.allCases, id: \.self) { tab in + section(for: tab) + .tabItem { + Label(tab.title, systemImage: tab.systemImage) + } + .tag(tab) + } + } + } + + /// Regular width (iPad, large iPhone landscape): two-column split view. + private var splitLayout: some View { + NavigationSplitView { + List(RootTab.allCases, id: \.self, selection: sidebarSelection) { tab in + Label(tab.title, systemImage: tab.systemImage) + .tag(tab) + } + .navigationTitle("DomainDig") + } detail: { + section(for: selectedTab) + } + } + + private var sidebarSelection: Binding<RootTab?> { + Binding( + get: { selectedTab }, + set: { selectedTab = $0 ?? selectedTab } + ) + } + + @ViewBuilder + private func section(for tab: RootTab) -> some View { + switch tab { + case .dashboard: + NavigationStack { + DashboardView(viewModel: viewModel) + } + case .audit: + NavigationStack { + AuditListView(viewModel: viewModel) + } + case .history: + NavigationStack { + HistoryView(viewModel: viewModel) + } + case .inspect: + ContentView(viewModel: viewModel) + case .settings: + NavigationStack { + SettingsView(viewModel: viewModel) + } + } + } + + // MARK: Intent / deep-link routing + private func consume(_ action: DomainDigDeepLink.Action?) { guard let action else { return } intentRouter.pendingAction = nil |
