swiftswiftuiswiftui-navigationstackipadosswiftui-navigationsplitview

How to still use sidebar after navigating inside a view padOS?


I have an app that i will run on iPad. I am setting up the sidebar but am stuck. I want to be able to select a page and then navigate within that selected page to other pages.

What is happening at the moment is, I am selecting between pages just fine in the sidebar and then in the main view, if I press a button that moves me away from that main view to another one, the sidebar becomes useless. I have to press "Back" to get back to the initial view that was chosen from the sidebar and then the sidebar becomes active again.

In the example app - if you go to "wx on Arrival" from the sidebar and then press the "Go settings" button in there to the "second layer" the sidebar doesn't work...

What I want to happen is: Regardless of how deep I go into a single selected sidebar view, I want to be able to select any of the sidebar options and go straight to what I am trying to go to. Thank you.

Here Is a very simple reproducible version of the problem, I apologise for the many pages:

import SwiftUI

@main
struct scrapAppApp: App {
    var body: some Scene {
        WindowGroup {
            MenuView()
        }
    }
}

funcs.swift

class AppState: ObservableObject {
    @Published var destination: String = ""
    @Published var path = NavigationPath()
    @Published var selectedItem: SidebarItem?
    @Published var readyToNavigate : Bool = false
    
}

MenuView.swift

enum SidebarItem: String, Identifiable, CaseIterable {
    case initialScreen = "Initial Screen"
    case arrivalWx = "Wx On Arrival"
    case legal = "Legal"

    var id: String { self.rawValue }

    var iconName: String {
        switch self {
        case .initialScreen:
            return "house"
        case .arrivalWx:
            return "cloud.sun"
        case .legal:
            return "doc.text"
        }
    }
}

struct MenuView: View {
    @StateObject private var app = AppState()
    
    var body: some View {
        NavigationSplitView {
            List(SidebarItem.allCases, id: \.self, selection: $app.selectedItem) { item in
                NavigationLink(value: item) {
                    Label(item.rawValue, systemImage: item.iconName)
                }
            }
            .listStyle(SidebarListStyle())
            .navigationTitle("Menu")
        } detail: {
            NavigationStack(path: $app.path) {
                if let selectedItem = app.selectedItem {
                    switch selectedItem {
                    case .initialScreen:
                        FirstPage()
                    case .arrivalWx:
                        SecondPage(app: app)
                    case .legal:
                        FourthPage(app: app)
                    }
                } else {
                    Text("Select an item")
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                }
            }
        }
    }
}

FirstPage.swift

struct FirstPage: View {
    var body: some View {
        Text("INITIAL SCREEN")
    }
}

SecondPage.swift

struct SecondPage: View {
    
    @ObservedObject var app: AppState
    
    var body: some View {
        Text("WX on ARRIVAL")
        Button("Go settings") {
            app.readyToNavigate = true
        }
        .navigationDestination(isPresented: $app.readyToNavigate) {
            ThirdPage()
        }
    }
}

ThirdPage.swift

struct ThirdPage: View {
    
    @ObservedObject var app = AppState()
    
    var body: some View {
        Text("Second Layer")
    }
}

FourthPage.swift

struct FourthPage: View {
    @ObservedObject var app: AppState
    
    var body: some View {
        Button("LEGAL") {
        }
    }
}

Solution

  • Here is my test code that works for me on iPadOS 17.5.1.

    struct MenuView: View {
        @StateObject private var app = AppState()
    
        var body: some View {
            NavigationSplitView {
                List(SidebarItem.allCases, selection: $app.selectedItem) { item in
                    NavigationLink(value: item) {
                        Label(item.rawValue, systemImage: item.iconName)
                    }
                }
                .listStyle(SidebarListStyle())
                .navigationTitle("Menu")
            } detail: {
                NavigationStack(path: $app.path) {
                    if let selectedItem = app.selectedItem {
                        switch selectedItem {
                        case .initialScreen:
                            FirstPage()
                        case .arrivalWx:
                            SecondPage()
                        case .legal:
                            FourthPage()
                        }
                    } else {
                        Text("Select an item")
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                    }
                }
                .environmentObject(app)  // <--- here
            }
        }
    }
    
    class AppState: ObservableObject {
        @Published var destination: String = ""
        @Published var path = NavigationPath()
        @Published var selectedItem: SidebarItem?
        @Published var readyToNavigate: Bool = false
    }
    
    enum SidebarItem: String, Identifiable, CaseIterable {
        case initialScreen = "Initial Screen"
        case arrivalWx = "Wx On Arrival"
        case legal = "Legal"
    
        var id: String { self.rawValue }
    
        var iconName: String {
            switch self {
            case .initialScreen:
                return "house"
            case .arrivalWx:
                return "cloud.sun"
            case .legal:
                return "doc.text"
            }
        }
    }
    
    struct FirstPage: View {
        @EnvironmentObject var app: AppState // not used
        
        var body: some View {
            Text("FirstPage INITIAL SCREEN")
        }
    }
    
    struct SecondPage: View {
        @EnvironmentObject var app: AppState
        
        var body: some View {
            Text("SecondPage WX on ARRIVAL")
            Button("Go settings") {
                app.readyToNavigate = true
            }
            .navigationDestination(isPresented: $app.readyToNavigate) {
                ThirdPage()
            }
        }
    }
    
    struct ThirdPage: View {
        @EnvironmentObject var app: AppState  // not used
        
        var body: some View {
            Text("ThirdPage Second Layer")
        }
    }
    
    struct FourthPage: View {
        @EnvironmentObject var app: AppState  // not used
        
        var body: some View {
            Button("FourthPage LEGAL") {
            }
        }
    }
    

    Note, I meant to say that in portrait mode, the sidebar disappears, but in landscape mode, it stays visible.