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) } }