Sidebar with different columns in SwiftUI

Viewed 866

I'm playing with the new Sidebar that has come with SwiftUI 2 and the possibility to navigate in large screens with three columns. An example about how it works can be found here: https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-sidebar-for-ipados

It works fine, but I would like to go one step forward and make some options of my main menu that show the three columns but other options just two.

Here an example of some demo code.

import SwiftUI

struct ContentView: View {
    var body: some View{
        NavigationView{
            List{
                Section(header: Text("Three columns")){
                    NavigationLink(
                        destination: ItemsView(),
                        label: {
                            Label("Animals",systemImage: "tortoise")
                        })
                    NavigationLink(
                        destination: ItemsView(),
                        label: {
                            Label("Animals 2",systemImage: "hare")
                        })
                }
                Section(header: Text("Two columns")){
                    NavigationLink(
                        destination: Text("I want to see here a single view, without detail"),
                        label: {
                            Label("Settings",systemImage: "gear")
                        })
                    NavigationLink(
                        destination: Text("I want to see here a single view, without detail"),
                        label: {
                            Label("Settings 2",systemImage: "gearshape")
                        })
                }
            }
            .listStyle(SidebarListStyle())
            .navigationBarTitle("App Menu")
            
            ItemsView()
            DetailView(animal: "default")
            
        }
    }
}

struct ItemsView: View{
    let animals = ["Dog", "Cat", "Lion", "Squirrel"]
    var body: some View{
        List{
            ForEach(animals, id: \.self){ animal in
                NavigationLink(
                    destination: DetailView(animal: animal)){
                    Text(animal)
                }
            }
        }
        .listStyle(PlainListStyle())
        .navigationTitle("Animals")
    }
}

struct DetailView: View{
    var animal: String
    var body: some View{
        VStack{
            Text("")
                .font(.title)
                .padding()
            Text(animal)
        }
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewLayout(.sizeThatFits)
    }
}

If you run the code in, for example, an iPad Pro (12,9-inch) in landscape mode, you can see the tree columns. First the App Menu (sidebar). If you click on one of the first two options (animals, and animals 2), you can see a list of animals (second column) and when you click on some animal, you reach the third column (detail view). However, I want to have only two columns when I click on the last two options of the menu (Settings and Settings 2). Any clue how to achieve it?

I've tried to hide that section if some of the first options in menu are not selected (with the selected parameter in NavigationLink), but without luck. It seems it is not possible (or I don't know) to know which option is selected in the sidebar.

Any idea is welcome!

3 Answers

It took me a few days to figure it out.

Test on different iPad device & multiple tasking mode, all works as expected.(iOS14+, haven't test on iOS13)

Minimal Example:

extension UISplitViewController {
    open override func viewDidLoad() {
        super.viewDidLoad()
        self.show(.primary) // show sidebar, this is the key, toke me days to find this...
        self.showsSecondaryOnlyButton = true
    }
}

struct ContentView: View {
    @State var column: Int = 3
    var body: some View {
        switch column {
        case 3: // Triple Column
            NavigationView {
                List {
                    HStack {
                        Text("Triple")
                    }
                    .onTapGesture {
                        column = 3
                    }
                    HStack {
                        Text("Double")
                    }
                    .onTapGesture {
                        column = 2
                    }
                }
                Text("Supplementary View")
                Text("Detail View")
            }
        default: // Double Column
            NavigationView {
                List {
                    HStack {
                        Text("Triple")
                    }
                    .onTapGesture {
                        column = 3
                    }
                    HStack {
                        Text("Double")
                    }
                    .onTapGesture {
                        column = 2
                    }
                }
                Text("Supplementary View")
            }
        }
    }
}

My another answer: set sidebar default selected item.

Combine with this two solution, I have built an 2&3 column co-exist style's app.

SwiftUI, selecting a row in a List programmatically

Here is a solution which uses a custom ViewModifier. It's working on iOS 14.2, 15.0 and 15.2. Since you are using SidebarListStyle and Label I didn't test for prior versions.

Testproject:

enum Item: Hashable {
    case animals, animals2, settings, settings2

    static var threeColumns = [Item.animals, .animals2]
    static var twoColumns = [Item.settings, .settings2]

    var title: String {
        switch self {
        case .animals:
            return "animals"
        case .animals2:
            return "animals2"
        case .settings:
            return "settings"
        case .settings2:
            return "settings2"
        }
    }

    var systemImage: String {
        switch self {
        case .animals:
            return "tortoise"
        case .animals2:
            return "hare"
        case .settings:
            return "gear"
        case .settings2:
            return "gearshape"
        }
    }
}

struct ContentView: View {

    @State var selectedItem: Item?

    var body: some View {
        NavigationView {
            List {
                Section(header: Text("Three columns")) {
                    ForEach(Item.threeColumns, id: \.self) { item in
                        NavigationLink(tag: item, selection: $selectedItem) {
                            ItemsView()
                        } label: {
                            Label(item.title.capitalized, systemImage: item.systemImage)
                        }
                    }
                }
                Section(header: Text("Two columns")) {
                    ForEach(Item.twoColumns, id: \.self) { item in
                        NavigationLink(
                            destination: Text("I want to see here a single view, without detail"),
                            label: {
                                Label(item.title, systemImage: item.systemImage)
                            })
                    }
                }
            }
            .listStyle(SidebarListStyle())
            .navigationBarTitle("App Menu")
        }
    }
}

struct ItemsView: View {
    let animals = ["Dog", "Cat", "Lion", "Squirrel"]
    var body: some View {
        List {
            ForEach(animals, id: \.self) { animal in
                NavigationLink(
                    destination: DetailView(animal: animal)) {
                        Text(animal)
                    }
            }
        }
        .listStyle(PlainListStyle())
        .navigationTitle("Animals")
    }
}

struct DetailView: View {
    var animal: String
    var body: some View {
        VStack {
            Text("")
                .font(.title)
                .padding()
            Text(animal)
        }
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewLayout(.sizeThatFits)
    }
}

private struct ColumnModifier: ViewModifier {
    let item: Item?

    func body(content: Content) -> some View {
        if item == .settings || item == .settings2 {
            content
            EmptyView()
        } else {
            content
            EmptyView()
            DetailView(animal: "default")
        }
    }
}

Suppose you want to create a triple-column view (with two sidebars and a detail view). Here is an example to show Projects in the first column, Files in second column and File Content in the last column.

The core step is to add three View()s in NavagationView { ... }.

import SwiftUI

struct MainView: View {
    var body: some View {
        ProjectsSidebar()
    }
}

struct ProjectsSidebar: View {
    var body: some View {
        NavigationView {
            List {
                ForEach([1, 2, 3], id: \.self) { project_id in
                    VStack {
                        NavigationLink {
                            FilesSidebar(project_id: project_id)
                                .navigationTitle("Project \(project_id)")
                                .navigationBarTitleDisplayMode(.inline)
                                .navigationViewStyle(.columns)
                        } label: {
                            Text("Project \(project_id)")
                        }
                    }
                }
            }
            .listStyle(.sidebar)

            .navigationTitle("Projects")
            .navigationBarTitleDisplayMode(.inline)

            FilesSidebar.DefaultView()

            DetailView.DefaultView()
        }
        .navigationViewStyle(.columns)
    }
}

struct FilesSidebar: View {
    var project_id: Int

    var body: some View {
        List {
            ForEach([1, 2, 3, 4], id: \.self) { file_id in
                NavigationLink {
                    DetailView(project_id: project_id, file_id: file_id)
                        .navigationTitle("File")
                        .navigationBarTitleDisplayMode(.inline)

                } label: {
                    Text("File \(file_id)")
                }
            }
        }
        .listStyle(.sidebar)
    }

    struct DefaultView: View {
        var body: some View {
            VStack {
                Text("Please select a project.")
            }
        }
    }
}

struct DetailView: View {
    var project_id: Int
    var file_id: Int

    var body: some View {
        VStack {
            Text("Project \(project_id) - File \(file_id)")
        }
    }

    struct DefaultView: View {
        var body: some View {
            VStack {
                Text("Please select a file.")
            }
        }
    }
}

first launch:

first launch

triple columns:

triple columns

select project and file:

select project and file

Related