Dismiss tvOS sidebar detail pages when sidebar selection changes

tvOS's sidebarAdaptable TabView leaves the previously-pushed detail view
visible after the user picks another sidebar item, until they manually
press Menu. Broadcast a notification on tab change so any pushed
TVSidebarDetailContainer dismisses itself, and reset each tab's
NavigationPath. Also drop a redundant inner NavigationStack in the tvOS
SettingsView so subpages register on the tab's outer stack.
This commit is contained in:
Arkadiusz Fal
2026-05-08 19:35:36 +02:00
parent 10bd7d09af
commit 5b9cd8c521
3 changed files with 58 additions and 4 deletions

View File

@@ -9,6 +9,10 @@
#if os(tvOS) #if os(tvOS)
import SwiftUI import SwiftUI
extension Notification.Name {
static let yatteeTVForcePopDetail = Notification.Name("yatteeTVForcePopDetail")
}
struct TVSidebarDetailContainer<Content: View>: View { struct TVSidebarDetailContainer<Content: View>: View {
let content: Content let content: Content
var systemImage: String? var systemImage: String?
@@ -65,6 +69,9 @@ struct TVSidebarDetailContainer<Content: View>: View {
} }
} }
} }
.onReceive(NotificationCenter.default.publisher(for: .yatteeTVForcePopDetail)) { _ in
dismiss()
}
} }
} }
#endif #endif

View File

@@ -614,6 +614,55 @@ struct UnifiedTabView: View {
selection = item selection = item
navigationCoordinator?.selectedSidebarItem = nil navigationCoordinator?.selectedSidebarItem = nil
} }
.onChange(of: selection) { oldValue, _ in
resetPath(for: oldValue)
// Force any pushed TVSidebarDetailContainer (used by Settings
// sub-pages and similar detail screens) to dismiss. Without this,
// tvOS's sidebarAdaptable TabView leaves the previously-pushed
// detail visible until the user manually presses Menu.
NotificationCenter.default.post(name: .yatteeTVForcePopDetail, object: nil)
}
}
/// Pops the previously-selected tab's NavigationStack to its root so its
/// pushed views don't linger after the user picks another sidebar item.
private func resetPath(for item: SidebarItem) {
switch item {
case .home:
homePath = NavigationPath()
case .search:
searchPath = NavigationPath()
case .subscriptionsFeed:
subscriptionsFeedPath = NavigationPath()
case .bookmarks:
bookmarksPath = NavigationPath()
case .history:
historyPath = NavigationPath()
case .manageChannels:
manageChannelsPath = NavigationPath()
case .playlistsList:
playlistsListPath = NavigationPath()
case .sources:
sourcesPath = NavigationPath()
case .settings:
settingsPath = NavigationPath()
case .openURL:
openURLPath = NavigationPath()
case .remoteControl:
remoteControlPath = NavigationPath()
case .continueWatching:
continueWatchingPath = NavigationPath()
case let .channel(channelID, _, _):
channelPaths[channelID] = NavigationPath()
case let .playlist(id, _):
playlistPaths[id] = NavigationPath()
case let .instance(id, _, _):
instancePaths[id] = NavigationPath()
case let .mediaSource(id, _, _):
mediaSourcePaths[id] = NavigationPath()
case .nowPlaying, .downloads:
break
}
} }
/// Applies the configured startup tab on first appearance. /// Applies the configured startup tab on first appearance.

View File

@@ -107,9 +107,8 @@ struct SettingsView: View {
#if os(tvOS) #if os(tvOS)
private var tvOSSettings: some View { private var tvOSSettings: some View {
NavigationStack { List {
List { if let appEnvironment {
if let appEnvironment {
NavigationLink { NavigationLink {
SourcesListView() SourcesListView()
} label: { } label: {
@@ -201,7 +200,6 @@ struct SettingsView: View {
.allowsHitTesting(false) .allowsHitTesting(false)
} }
.accessibilityIdentifier("settings.view") .accessibilityIdentifier("settings.view")
}
} }
#endif #endif