summaryrefslogtreecommitdiff
path: root/DomainDig/RootTabView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-17 08:36:05 -0500
committerChristian Cleberg <[email protected]>2026-07-17 10:12:36 -0500
commitb4b94d54cbed9334a28cdcd70d3a8a0fd06e04ef (patch)
tree8fa4b408729da32ec2c547081ec2bba13b893fa2 /DomainDig/RootTabView.swift
parent065e1e77ff00e8f9c8785393b5eba6c3e0bb2bb1 (diff)
downloaddomain-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.swift136
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