ricobeck
ricobeck

Reputation: 171

How do I handle selection in NavigationView on macOS correctly?

I want to build a trivial macOS application with a sidebar and some contents according to the selection in the sidebar.

I have a MainView which contains a NavigationView with a SidebarListStyle. It contains a List with some NavigationLinks. These have a binding for a selection.

I would expect the following things to work:

  1. When I start my application the value of the selection is ignored. Neither is there a highlight for the item in the sidebar nor a content in the detail pane.

  2. When I manually select an item in the sidebar it should be possible to navigate via up/down arrow keys between the items. This does not work as the selection / highlight disappears.

  3. When I update the value of the selection-binding it should highlight the item in the list which doesn't happen.

Here is my example implementation:

enum DetailContent: Int, CaseIterable {
    case first, second, third
}
extension DetailContent: Identifiable {
    var id: Int { rawValue }
}

class NavigationRouter: ObservableObject {
    @Published var selection: DetailContent?
}

struct DetailView: View {
    @State var content: DetailContent
    @EnvironmentObject var navigationRouter: NavigationRouter

    var body: some View {
        VStack {
            Text("\(content.rawValue)")
            Button(action: { self.navigationRouter.selection = DetailContent.allCases.randomElement()!}) {
                Text("Take me anywhere")
            }
        }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct MainView: View {
    @ObservedObject var navigationRouter = NavigationRouter()
    @State var detailContent: DetailContent? = .first

    var body: some View {
        NavigationView {
            List {
                Section(header: Text("Section")) {
                    ForEach(DetailContent.allCases) { item in
                        NavigationLink(
                            destination: DetailView(content: item),
                            tag: item,
                            selection: self.$detailContent,
                            label: { Text("\(item.rawValue)") }
                        )
                    }
                }
            }
            .frame(minWidth: 250, maxWidth: 350)
        }
            .environmentObject(navigationRouter)
            .listStyle(SidebarListStyle())
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .onReceive(navigationRouter.$selection) { output in
                self.detailContent = output
            }
    }
}

The EnvironmentObject is used to propagate the change from inside the DetailView. If there's a better solution I'm very happy to hear about it.

So the question remains: What am I doing wrong that this happens? I had some hope that with Xcode 11.5 Beta 1 this would go away but that's not the case.

Upvotes: 3

Views: 426

Answers (1)

ricobeck
ricobeck

Reputation: 171

After finding the tutorial from Apple it became clear that you don't use NavigiationLink on macOS. Instead you bind the list and add two views to NavigationView.

With these updates to MainView and DetailView my example works perfectly:

struct DetailView: View {
    @Binding var content: DetailContent?
    @EnvironmentObject var navigationRouter: NavigationRouter

    var body: some View {
        VStack {
            Text("\(content?.rawValue ?? -1)")
            Button(action: { self.navigationRouter.selection = DetailContent.allCases.randomElement()!}) {
                Text("Take me anywhere")
            }
        }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct MainView: View {
    @ObservedObject var navigationRouter = NavigationRouter()
    @State var detailContent: DetailContent?

    var body: some View {
        NavigationView {
            List(selection: $detailContent) {
                Section(header: Text("Section")) {
                    ForEach(DetailContent.allCases) { item in
                        Text("\(item.rawValue)")
                            .tag(item)
                    }
                }
            }
                .frame(minWidth: 250, maxWidth: 350)
                .listStyle(SidebarListStyle())
            DetailView(content: $detailContent)
        }
            .environmentObject(navigationRouter)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .onReceive(navigationRouter.$selection) { output in
                DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
                    self.detailContent = output
                }
            }
    }
}

Upvotes: 2

Related Questions