From 4fbe1bacdd228a6ee959a36a377c6471370c4244 Mon Sep 17 00:00:00 2001 From: Alan Ye Date: Tue, 14 Apr 2026 21:39:29 +0800 Subject: [PATCH] fix: restore deep link navigation in RootTabView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deep links stopped working on iPhone when NavSplitView was replaced by RootTabView — the tab-switching logic was never carried over. Now RootTabView observes URLSchemeHandler properties and switches tabs accordingly. Club deep links push ClubInfoView onto the Explore tab via NavigationPath. Also removes dead navigateToReflection property (never set, no route). --- Outspire/Core/Services/URLSchemeHandler.swift | 1 - .../CAS/Views/ClubActivitiesView.swift | 9 ++++ .../Features/Main/Views/NavSplitView.swift | 1 - .../Features/Main/Views/RootTabView.swift | 45 +++++++++++++++++-- docs/CHANGELOG.md | 15 +++++++ docs/wiki/Navigation-Deep-Linking.md | 8 ++-- docs/wiki/Services-Reference.md | 1 - 7 files changed, 70 insertions(+), 10 deletions(-) diff --git a/Outspire/Core/Services/URLSchemeHandler.swift b/Outspire/Core/Services/URLSchemeHandler.swift index 5a4d508..5faae36 100644 --- a/Outspire/Core/Services/URLSchemeHandler.swift +++ b/Outspire/Core/Services/URLSchemeHandler.swift @@ -11,7 +11,6 @@ class URLSchemeHandler: ObservableObject { @Published var navigateToClassTable = false @Published var navigateToClub: String? @Published var navigateToAddActivity: String? - @Published var navigateToReflection: String? // Add a property to signal that sheets should be closed @Published var closeAllSheets = false diff --git a/Outspire/Features/CAS/Views/ClubActivitiesView.swift b/Outspire/Features/CAS/Views/ClubActivitiesView.swift index 83340c7..40c7327 100644 --- a/Outspire/Features/CAS/Views/ClubActivitiesView.swift +++ b/Outspire/Features/CAS/Views/ClubActivitiesView.swift @@ -81,6 +81,15 @@ struct ClubActivitiesView: View { } } }) + .onChange(of: urlSchemeHandler.navigateToAddActivity) { _, newClubId in + if let activityClubId = newClubId { + viewModel.setSelectedGroupById(activityClubId) + showingAddRecordSheet = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + urlSchemeHandler.navigateToAddActivity = nil + } + } + } .onChange(of: viewModel.isLoadingActivities) { handleLoadingChange() } diff --git a/Outspire/Features/Main/Views/NavSplitView.swift b/Outspire/Features/Main/Views/NavSplitView.swift index c924dee..ac918de 100644 --- a/Outspire/Features/Main/Views/NavSplitView.swift +++ b/Outspire/Features/Main/Views/NavSplitView.swift @@ -95,7 +95,6 @@ struct NavSplitView: View { .onChange(of: urlSchemeHandler.navigateToAddActivity) { _, clubId in if clubId != nil { selectedView = .clubActivities } } - .onChange(of: urlSchemeHandler.navigateToReflection) { _, _ in selectedView = .clubReflections } .id(refreshID) .task { checkOnboardingStatus() diff --git a/Outspire/Features/Main/Views/RootTabView.swift b/Outspire/Features/Main/Views/RootTabView.swift index a1fdd91..cdd7ccb 100644 --- a/Outspire/Features/Main/Views/RootTabView.swift +++ b/Outspire/Features/Main/Views/RootTabView.swift @@ -13,6 +13,9 @@ struct RootTabView: View { @State private var hasCheckedOnboarding = false enum MainTab: Hashable { case today, classtable, activities, search } + enum DeepLinkRoute: Hashable { case clubInfo } + + @State private var explorePath = NavigationPath() var body: some View { Group { @@ -36,6 +39,24 @@ struct RootTabView: View { .task { checkOnboardingStatus() } + .onChange(of: urlSchemeHandler.navigateToToday) { _, newValue in + if newValue { selectedTab = .today } + } + .onChange(of: urlSchemeHandler.navigateToClassTable) { _, newValue in + if newValue { selectedTab = .classtable } + } + .onChange(of: urlSchemeHandler.navigateToClub) { _, clubId in + if clubId != nil { + selectedTab = .search + explorePath = NavigationPath() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + explorePath.append(DeepLinkRoute.clubInfo) + } + } + } + .onChange(of: urlSchemeHandler.navigateToAddActivity) { _, clubId in + if clubId != nil { selectedTab = .activities } + } .onReceive( NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) ) { _ in @@ -69,8 +90,14 @@ struct RootTabView: View { } Tab("Explore", systemImage: "square.grid.2x2", value: MainTab.search) { - NavigationStack { + NavigationStack(path: $explorePath) { ExtraView() + .navigationDestination(for: DeepLinkRoute.self) { route in + switch route { + case .clubInfo: + ClubInfoView() + } + } } } } @@ -103,8 +130,14 @@ struct RootTabView: View { } Tab("Explore", systemImage: "square.grid.2x2", value: MainTab.search) { - NavigationStack { + NavigationStack(path: $explorePath) { ExtraView() + .navigationDestination(for: DeepLinkRoute.self) { route in + switch route { + case .clubInfo: + ClubInfoView() + } + } } } } @@ -133,8 +166,14 @@ struct RootTabView: View { .tabItem { Label("Activities", systemImage: "checklist.checked") } .tag(MainTab.activities) - NavigationStack { + NavigationStack(path: $explorePath) { ExtraView() + .navigationDestination(for: DeepLinkRoute.self) { route in + switch route { + case .clubInfo: + ClubInfoView() + } + } } .tabItem { Label("Explore", systemImage: "square.grid.2x2") } .tag(MainTab.search) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a470f1e..9ea04b2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,20 @@ # Documentation Changelog +## 2026-04-14 (update 3) + +### Fixed +- Deep link navigation in RootTabView -- all `outspire://` deep links now switch to the correct tab + - `outspire://today` and `outspire://classtable` switch tabs + - `outspire://club/{id}` switches to Explore tab and pushes ClubInfoView via NavigationPath + - `outspire://addactivity/{id}` switches to Activities tab +- Added `.onChange` handler for `navigateToAddActivity` in ClubActivitiesView (previously only handled in `.onAppear`) +- Removed dead `navigateToReflection` property from URLSchemeHandler (never set, no route, no view response) +- Removed dead `.onChange(of: navigateToReflection)` from NavSplitView + +### Changed +- Navigation-Deep-Linking.md updated to reflect working deep link flow and removed reflection route +- Explore tab NavigationStack now uses NavigationPath for programmatic deep link navigation + ## 2026-04-14 (update 2) ### Added diff --git a/docs/wiki/Navigation-Deep-Linking.md b/docs/wiki/Navigation-Deep-Linking.md index b5ad290..e65a021 100644 --- a/docs/wiki/Navigation-Deep-Linking.md +++ b/docs/wiki/Navigation-Deep-Linking.md @@ -19,7 +19,7 @@ | Activities | "Activities" + sparkles icon | CAS views | | Explore | "Explore" + safari icon | `ExtraView` (quick links grid) | -Each tab contains its own `NavigationStack` to preserve navigation state independently. +Each tab contains its own `NavigationStack` to preserve navigation state independently. The Explore tab uses a `NavigationPath`-bound `NavigationStack` to support programmatic push navigation for deep links (e.g., `outspire://club/{id}` pushes `ClubInfoView` onto the Explore stack). ## iPad Navigation (DEAD CODE) @@ -39,7 +39,6 @@ Each tab contains its own `NavigationStack` to preserve navigation state indepen | `outspire://classtable` | Navigate to timetable | | `outspire://club/{clubId}` | Open club info | | `outspire://addactivity/{clubId}` | Open add activity sheet for club | -| `outspire://reflection/{reflectionId}` | Open specific reflection | ### Universal Links @@ -69,10 +68,11 @@ URLSchemeHandler publishes navigation triggers as `@Published` properties: - `navigateToClassTable: Bool` - `navigateToClub: String?` - `navigateToAddActivity: String?` -- `navigateToReflection: String?` - `closeAllSheets: Bool` -Views observe these and respond accordingly. The handler includes: +`RootTabView` observes these properties via `.onChange` and switches to the appropriate tab. For `navigateToClub`, it also programmatically pushes `ClubInfoView` onto the Explore tab's `NavigationPath`. Individual views (`ClubInfoView`, `ClubActivitiesView`, `TodayView`) respond to the relevant properties in `.onAppear`/`.onChange` to complete the navigation. + +The handler includes: - **Queued navigation** -- If app isn't ready, URL is queued and fired after 0.5s - **Reset-and-set pattern** -- Navigation flags reset before being set to detect same-destination re-navigation - **Sheet dismissal** -- `closeAllSheets` flag resets after 0.5s diff --git a/docs/wiki/Services-Reference.md b/docs/wiki/Services-Reference.md index 6a861e9..d0f82e5 100644 --- a/docs/wiki/Services-Reference.md +++ b/docs/wiki/Services-Reference.md @@ -146,7 +146,6 @@ Automatic reauthentication on 401, 302, or HTML responses. | `navigateToClassTable` | `Bool` | Navigate to timetable | | `navigateToClub` | `String?` | Club ID to open | | `navigateToAddActivity` | `String?` | Club ID for new activity | -| `navigateToReflection` | `String?` | Reflection ID to open | | Method | Description | |--------|-------------|