From cf90eb4d4eeaa4e8a5d154e4d9da5a5936e5bf1e Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 4 Oct 2026 23:50:39 +0200 Subject: [PATCH] Dismiss sheets when navigation arrives from outside the app Tapping a new-videos notification while Home's Settings sheet was up switched to the Subscriptions feed underneath the sheet, so the app came back still showing Settings. The existing dismissSettings() hook was never called. NavigationCoordinator.dismissPresentations() bumps a generation that the main window's root sheet host and screen sheets (Home's Settings, Remote Devices, Customize Home; Subscriptions' view options and data) observe via .dismissesOnExternalNavigation(_:). The notification, deep links and Handoff call it; in-app navigation and layout swaps do not. The expanded player keeps its own sheets. The notification also collapses the expanded player so the feed it opens is visible. --- Yattee/ContentView.swift | 5 ++- .../NotificationManager.swift | 4 +- .../Navigation/NavigationCoordinator.swift | 21 +++++++--- .../Navigation/PresentationRouter.swift | 40 +++++++++++++++++-- Yattee/Views/Home/HomeView.swift | 6 +-- .../Subscriptions/SubscriptionsView.swift | 4 ++ Yattee/YatteeApp.swift | 3 ++ 7 files changed, 69 insertions(+), 14 deletions(-) diff --git a/Yattee/ContentView.swift b/Yattee/ContentView.swift index b37f1645..7cb43e9b 100644 --- a/Yattee/ContentView.swift +++ b/Yattee/ContentView.swift @@ -26,7 +26,10 @@ struct ContentView: View { } // Shared sheets for the main window. The expanded player hosts its own, // and takes global requests while it is up. - .appSheetHost(isTopLevel: appEnvironment?.navigationCoordinator.isPlayerExpanded != true) + .appSheetHost( + isTopLevel: appEnvironment?.navigationCoordinator.isPlayerExpanded != true, + dismissesOnExternalNavigation: true + ) .toastOverlay() } diff --git a/Yattee/Services/BackgroundRefresh/NotificationManager.swift b/Yattee/Services/BackgroundRefresh/NotificationManager.swift index eeeaa900..51cadd91 100644 --- a/Yattee/Services/BackgroundRefresh/NotificationManager.swift +++ b/Yattee/Services/BackgroundRefresh/NotificationManager.swift @@ -264,7 +264,9 @@ extension NotificationManager: UNUserNotificationCenterDelegate { func handlePendingNavigation(using coordinator: NavigationCoordinator) { guard UserDefaults.standard.bool(forKey: Self.pendingNavigationKey) else { return } UserDefaults.standard.set(false, forKey: Self.pendingNavigationKey) - coordinator.navigateToSubscriptions() + coordinator.dismissPresentations() + // The feed has to be visible: collapse the player that would cover it. + coordinator.navigateToSubscriptions(collapsePlayer: true) } #endif } diff --git a/Yattee/Services/Navigation/NavigationCoordinator.swift b/Yattee/Services/Navigation/NavigationCoordinator.swift index 89f7e23b..8b08f009 100644 --- a/Yattee/Services/Navigation/NavigationCoordinator.swift +++ b/Yattee/Services/Navigation/NavigationCoordinator.swift @@ -32,8 +32,10 @@ final class NavigationCoordinator { } } - /// Trigger to dismiss settings sheet (incremented when dismiss is needed). - var dismissSettingsTrigger = 0 + /// Bumped when navigation arriving from outside the app (a notification, + /// a deep link, Handoff) needs the main window's sheets out of the way. + /// Sheets observe it via `.dismissesOnExternalNavigation(_:)`. + var dismissPresentationsGeneration = 0 /// Whether the mini player is expanded to full screen. var isPlayerExpanded = false { @@ -235,9 +237,12 @@ final class NavigationCoordinator { // MARK: - Sheet Presentation - /// Dismiss the settings sheet from anywhere in the app. - func dismissSettings() { - dismissSettingsTrigger += 1 + /// Dismiss the main window's sheets so external navigation lands on a + /// visible screen instead of changing tabs underneath a sheet. Call it + /// only from external entry points: in-app navigation and layout swaps + /// must keep an open sheet. + func dismissPresentations() { + dismissPresentationsGeneration += 1 } // MARK: - Player Actions @@ -276,7 +281,11 @@ final class NavigationCoordinator { // MARK: - Convenience Navigation /// Navigate to the subscriptions tab/feed. - func navigateToSubscriptions() { + func navigateToSubscriptions(collapsePlayer: Bool = false) { + if collapsePlayer, isPlayerExpanded { + isPlayerCollapsing = true + isPlayerExpanded = false + } router.select(.subscriptionsFeed) } diff --git a/Yattee/Services/Navigation/PresentationRouter.swift b/Yattee/Services/Navigation/PresentationRouter.swift index 5a66c4f2..680d4cc6 100644 --- a/Yattee/Services/Navigation/PresentationRouter.swift +++ b/Yattee/Services/Navigation/PresentationRouter.swift @@ -95,6 +95,9 @@ private struct AppSheetHost: ViewModifier { /// Whether this host presents global requests (`PresentationRouter.request`). let isTopLevel: Bool + /// Whether external navigation dismisses this host's sheet. Only the main + /// window's root host: the expanded player keeps its own sheets. + let dismissesOnExternalNavigation: Bool @State private var sheet: AppSheet? @@ -104,11 +107,20 @@ private struct AppSheetHost: ViewModifier { .sheet(item: $sheet) { sheet in sheetContent(for: sheet) } + // Declared before the request observer so a deep link that both + // dismisses and presents ends with its own sheet up. + .onChange(of: dismissGeneration) { _, _ in + sheet = nil + } .onChange(of: pendingRequestID, initial: true) { _, _ in takePendingRequest() } } + private var dismissGeneration: Int? { + dismissesOnExternalNavigation ? appEnvironment?.navigationCoordinator.dismissPresentationsGeneration : nil + } + private var pendingRequestID: String? { isTopLevel ? appEnvironment?.navigationCoordinator.presenter.request?.id : nil } @@ -144,7 +156,8 @@ private struct AppSheetHost: ViewModifier { #endif } } - // The next level: sheets opened from inside this one. + // The next level: sheets opened from inside this one. It goes away + // with this sheet, so it needn't observe external navigation itself. .appSheetHost(isTopLevel: false) .optionalAppEnvironment(appEnvironment) } @@ -176,7 +189,28 @@ extension View { /// - Parameter isTopLevel: true for the hosts that present global requests: /// the app root while the player is collapsed, the expanded player while /// it is up. - func appSheetHost(isTopLevel: Bool) -> some View { - modifier(AppSheetHost(isTopLevel: isTopLevel)) + /// - Parameter dismissesOnExternalNavigation: true for the main window's + /// root host, whose sheet external navigation (notification, deep link, + /// Handoff) dismisses. + func appSheetHost(isTopLevel: Bool, dismissesOnExternalNavigation: Bool = false) -> some View { + modifier(AppSheetHost(isTopLevel: isTopLevel, dismissesOnExternalNavigation: dismissesOnExternalNavigation)) + } + + /// Dismisses a screen's own sheet when navigation arrives from outside the + /// app, so it doesn't stay up over the screen the navigation moved to. + func dismissesOnExternalNavigation(_ isPresented: Binding) -> some View { + modifier(ExternalNavigationDismissal(isPresented: isPresented)) + } +} + +private struct ExternalNavigationDismissal: ViewModifier { + @Environment(\.appEnvironment) private var appEnvironment + @Binding var isPresented: Bool + + func body(content: Content) -> some View { + content + .onChange(of: appEnvironment?.navigationCoordinator.dismissPresentationsGeneration) { _, _ in + isPresented = false + } } } diff --git a/Yattee/Views/Home/HomeView.swift b/Yattee/Views/Home/HomeView.swift index 45c41fc0..f97ff1a4 100644 --- a/Yattee/Views/Home/HomeView.swift +++ b/Yattee/Views/Home/HomeView.swift @@ -197,13 +197,12 @@ struct HomeView: View { SettingsView() .liquidGlassSheetContent(sourceID: "homeSettings", in: sheetTransition) } - .onChange(of: appEnvironment?.navigationCoordinator.dismissSettingsTrigger) { - showingSettings = false - } + .dismissesOnExternalNavigation($showingSettings) #endif .sheet(isPresented: $showingRemoteControl) { RemoteDevicesSheet() } + .dismissesOnExternalNavigation($showingRemoteControl) #if !os(tvOS) .sheet(isPresented: $showingCustomizeHome) { NavigationStack { @@ -220,6 +219,7 @@ struct HomeView: View { .frame(minWidth: 500, minHeight: 450) #endif } + .dismissesOnExternalNavigation($showingCustomizeHome) #endif .onAppear { loadData() diff --git a/Yattee/Views/Subscriptions/SubscriptionsView.swift b/Yattee/Views/Subscriptions/SubscriptionsView.swift index c9717345..8c82bec2 100644 --- a/Yattee/Views/Subscriptions/SubscriptionsView.swift +++ b/Yattee/Views/Subscriptions/SubscriptionsView.swift @@ -553,6 +553,10 @@ struct SubscriptionsView: View { .liquidGlassSheetContent(sourceID: "subscriptionsViewOptions", in: sheetTransition) } #endif + .dismissesOnExternalNavigation($showViewOptions) + #if os(macOS) + .dismissesOnExternalNavigation($showSubscriptionsData) + #endif .onAppear { // Synchronous load for local accounts so the sidebar is // present in the first layout pass (avoids layout jump). diff --git a/Yattee/YatteeApp.swift b/Yattee/YatteeApp.swift index ea9c81b9..b0f409b6 100644 --- a/Yattee/YatteeApp.swift +++ b/Yattee/YatteeApp.swift @@ -73,9 +73,11 @@ struct YatteeApp: App { .handlesExternalEvents(preferring: ["*"], allowing: ["*"]) #endif .onOpenURL { url in + appEnvironment.navigationCoordinator.dismissPresentations() handleDeepLink(url) } .onContinueUserActivity(HandoffManager.activityType) { activity in + appEnvironment.navigationCoordinator.dismissPresentations() handleContinuedActivity(activity) } .onAppear { @@ -93,6 +95,7 @@ struct YatteeApp: App { } .onReceive(NotificationCenter.default.publisher(for: .continueUserActivity)) { notification in if let activity = notification.object as? NSUserActivity { + appEnvironment.navigationCoordinator.dismissPresentations() handleContinuedActivity(activity) } }