swiftuiswiftui-navigationlinkswiftui-navigationsplitviewnavigationsplitview

Control NavigationLink in NavigationSplitView from multiple places (List and Picker)


I have a list within a NavigationSplitView. The list has NavigationLinks, which when selected, load a detail view:

List(selection: $selectedMode) {
    ForEach(modes, id: \.self) { modeInfo in
        NavigationLink {
            DashboardView(
                parameters:
                    DashboardParameters(
                        mode: selectedMode,
                        moment: selectedFromMoment
                    )
            )
        } label: {
            Text(modeInfo.name)
        }
    }
}

and then I have a picker where I want to also be able to update the DashboardView in the detail section:

Picker("", selection: $selectedFromMoment) {
    ForEach(Moment.allCases.filter { moment in
        moment != Moment.now
    }, id: \.name) { moment in
        Text(moment.name).tag(moment)
    }
}.onChange(of: selectedFromMoment) { oldValue, newValue in
    //I want to update detail view from here
}

Note that the view loaded (DashboardView) takes an object which has two parameters (one set from the List and one from a separate picker).

How can I get the NavigationLink to trigger also when the picker is changed? Basically, I need to load a new detail view whenever the lists changes or the picker changes.

Here is my current solution, which seems like a HUGE hack:

.onChange(of: selectedFromMoment) { oldValue, newValue in
    let m = selectedMode
    selectedMode = nil
    Task {
        try await Task.sleep(for: .seconds(0.01))
        selectedMode = m
    }
}

Basically, when a new value is chosen from the Picker, I clear and reset the selected item in the list, which forces the navigationlink in the list to fire again. I have to wait a tick though, as if I clear and reset in the same loop, it doesnt work.


Solution

  • As long as your dashboard view depends on @State variables, it should get redrawn whenever they change.

    Here's a quick example of how it works with a custom Struct that contains the selection for both the list and the picker. When you pass this struct to the detail view, it updates automatically whenever one of the values inside the struct changes.

    struct ContentView: View {
        private enum PickerValue: String, CaseIterable {
            case low, medium, high
        }
        
        private struct DashboardData {
            var listSelection: Int
            var pickerSelection: PickerValue
        }
        
        @State private var data = DashboardData(listSelection: 0, pickerSelection: .high)
        
        var body: some View {
            NavigationSplitView {
                VStack {
                    List(0..<10, id: \.self, selection: self.$data.listSelection) { index in
                        NavigationLink("Go to \(index)", value: index)
                    }
                    
                    Picker("moment", selection: self.$data.pickerSelection) {
                        ForEach(PickerValue.allCases, id:\.self) { moment in
                            Text(moment.rawValue)
                        }
                    }
                    .padding()
                }
            } detail: {
                self.getDetailView(data: self.data)
            }
        }
    
        private func getDetailView(data: DashboardData) -> some View {
            Text("We are in \(data.listSelection) with moment \(data.pickerSelection.rawValue)")
        }
    }