Проблема з використанням NavigationSplitView та NavigationStack у SwiftUI може стати викликом для розробників. У даній статті ми розглянемо ситуацію, коли вкладені NavigationStack в межах NavigationSplitView поводяться неочікувано. У нашому додатку ми спостерігаємо проблему, коли кожен NavigationStack в межах NavigationSplitView прив’язаний до різних шляхів, але прив’язаний шлях не дотримується так, як ми очікували. У цьому додатку є 4 маршрути: Inbox/Inbox (головний шлях) Inbox/Message Outbox/Outbox (головний шлях) Outbox/Message Навігаційний маршрутизатор відповідає за оновлення стану таким чином: бічна панель NavigationSplitView прив’язана до категорії, шлях NavigationStack прив’язаний до 2 різних шляхів, в залежності від того, яка категорія обрана в бічній панелі. Ми зіткнулися з проблемою, коли ми хочемо змінити маршрути: [Inbox/Message] -> [Outbox/Message] (зверніть увагу на зміну шляху, де глибина більше, ніж корінь) Це призводить нас до пункту призначення в UI: [Outbox/Outbox] (зверніть увагу на те, що зміна здійснюється, але інтерфейс користувача перебуває в кореневому режимі) Здається, що NavigationSplitView: враховує зміну категорії в бічній панелі активно встановлює поточний прив’язаний шлях навігації на порожній шлях: [] не дотримується нового прив’язаного шляху при глибині Не знаєте, як можна було б досягти описаного вище? Я боюся, що мені доведеться написати обгортку UIKit для того, що, здається, повинно бути можливим за кодом, наданим тут.
1 |
<p>@main struct AppRouterApp: App { var body: some Scene { WindowGroup { ContentView() } } } struct ContentView: View { var body: some View { TopLevelNavigationView() .environmentObject(NavigationRouter()) } } // The different sidebar options enum NavigationSection: String, Hashable, Identifiable, Equatable { case inbox case outbox var id: Self { self } } // The different detail options enum NavigationDestination: String, Hashable, Identifiable, Equatable { case inbox case outbox case message var id: Self { self } } @MainActor final class NavigationRouter: ObservableObject { // Sidebar state @Published var section: NavigationSection? = .inbox // State of path when inbox section is selected @Published var inboxPath: [NavigationDestination] = [] // State of path when outbox section is selected @Published var outboxPath: [NavigationDestination] = [] private var cancellables = Set<AnyCancellable>() init() { $inboxPath .map { $0.map { $0.rawValue }} .sink { print("Inbox", $0) } .store(in: &cancellables) $outboxPath .map { $0.map { $0.rawValue }} .sink { print("Outbox", $0) } .store(in: &cancellables) } func routeTo(section: NavigationSection, destination: NavigationDestination) { print("routing to: \(section.rawValue) destination: \(destination.rawValue)") defer { self.section = section } switch (section, destination) { case (.inbox, .inbox): guard !self.inboxPath.isEmpty else { print("already at destination") return } self.inboxPath.removeAll() case (.inbox, .message): guard !self.inboxPath.contains(.message) else { print("already at destination") return } self.inboxPath.append(.message) case (.inbox, .outbox): print("invalid route") case (.outbox, .inbox): print("invalid route") case (.outbox, .message): guard !self.outboxPath.contains(.message) else { print("already at destination") return } self.outboxPath.append(.message) case (.outbox, .outbox): guard !self.outboxPath.isEmpty else { print("already at destination") return } self.outboxPath.removeAll() } } } struct TopLevelNavigationView: View { @EnvironmentObject private var navigationRouter: NavigationRouter var body: some View { NavigationSplitView { List([NavigationSection.inbox, .outbox], selection: $navigationRouter.section) { section in Label("\(section.rawValue.capitalized)", systemImage: "mail") .id(section) } } detail: { switch navigationRouter.section { case .inbox: StackNavigationView(path: $navigationRouter.inboxPath) { InboxView() } .environmentObject(navigationRouter) case .outbox: StackNavigationView(path: $navigationRouter.outboxPath) { OutboxView() } .environmentObject(navigationRouter) case nil: Text("Make selection in sidebar") } } } } struct StackNavigationView<Root: View>: View { @EnvironmentObject private var router: NavigationRouter @Binding var path: [NavigationDestination] let content: () -> Root @State private var redrawToken = UUID() init(path: Binding<[NavigationDestination]>, @ViewBuilder content: @escaping () -> Root) { self._path = path self.content = content } var body: some View { NavigationStack(path: $path.animation(.linear(duration: 0))) { content() .navigationDestination(for: NavigationDestination.self) { destination in switch destination { case .inbox: InboxView() case .outbox: OutboxView() case .message: MessageView() } } } .id(redrawToken) .onAppear { /* We encounter an issue where the nested each NavigationStack within a NavigationSplitView is bound to a different path but the bound path isn't honored in a way that we would expect. path In this sample app there are 4 routes: - Inbox/Inbox (path root) - Inbox/Message - Outbox/Outbox (path root) - Outbox/Message The NavigationRouter is responsible for updating state such that: - the NavigationSplitView **sidebar** is bound to categorycategory is selected. - the NavigationStack **path** is bound to 2 different paths, depending on which sidebar category is selected. We encounter an issue when we want to change routes: [Inbox/Message] -> [Outbox/Message] _(note the change to a path where depth is deeper than root)_ Brings us to a route destination in the UI: [Outbox/Outbox] _(note the change is made but the UI is at root)_ What appears to be happening is that the NavigationSplitView: - honors the category change is honored in the sidebar - actively sets the current bound navigation path to an empty path: `[]` - **does not honor** the newly bound destination path at depth If I introduce this Task as a buffer you will see that after waiting a second a message tells the view to re-draw. By virtue of waiting long enough for the NavigationSplitView to have finished updating path state and presentation, we can actually grab the REAL navigation path. Un-comment the following task to see the intended behavior. */ } } } struct InboxView: View { @EnvironmentObject private var router: NavigationRouter var body: some View { ScrollView { Text("Inbox") Button("Open Inbox Message") { router.routeTo(section: .inbox, destination: .message) } Button("Open Inbox") { router.routeTo(section: .inbox, destination: .inbox) } Button("Open Outbox Message") { router.routeTo(section: .outbox, destination: .message) } Button("Open Outbox") { router.routeTo(section: .outbox, destination: .out }</p> |