swiftsidebarswiftui-navigationlinkdetailviewnavigationsplitview

Why does NavigationLink break my NavigationSplitView


Below I have created a simple NavigationSplitView for MacOS using Swift. Both the sidebar and the detail page rely on an Observable value. When I switch that observable value both sidebar and detail update accordingly....... Until the sidebar invokes a NavigationLink to same detail page. At that point changing the Observed value only updates the sidebar and not the detail page. This is a major pain for my App navigation and I am wondering if anyone knows why the NavigationSplitView detail page stops responding to the change in the observed value. I guess it could be stacking views on top of each other when a navigationLink is called.

import SwiftUI
import Foundation
internal import Combine

final class ModelData: ObservableObject {
    @Published var value: Int = 1
}

struct ContentView: View {
    @EnvironmentObject var modelData: ModelData
    var body: some View {
        NavigationSplitView{
                sidebarPage(v: modelData.value)
        }detail:{
                detailPage(v: modelData.value)
        }
    }
}


struct sidebarPage: View{
    @EnvironmentObject var modelData: ModelData
    var v: Int
    var body: some View {
        //Show an indication that NavigationSplitView Sidebar is updating when modelData.selectedTab is altered
        Text("Sidebar version")
        Text("\(v)").font(.largeTitle)
        //Switch button which changes the value of the modelData.selectedTab which will be seen to have two behaviours
        //When pressed repeatedly it changes the NavigationSplitView Sidebar and detail view, simply by altering the modelData.selectedTab value
        //This behaviour then changes once a NavigationLink is used
        Text("Switching changes sidebar and detail pages").font(.caption)
        Button("Switch", action: {modelData.value = 3 - modelData.value})
        
        //NavigationLink alter the detail page
        Text("Until a NavigationLink is invoked").font(.caption)
        NavigationLink ("Detail Page 1", destination: detailPage(v: 1))
        NavigationLink ("Detail Page 2", destination: detailPage(v: 2))
        
    }
}
struct detailPage: View{
    @EnvironmentObject var modelData: ModelData
    var v: Int
    
    var body: some View {
        //Show a change in v in detail page
        Text("Detail Page")
        Text("\(v)").font(.largeTitle)
            .onChange(of: v) {
                //when the value of the page changes, update the modelData.selectedTab value accordingly.
                //This changes NavigationSplitView Sidebar page
                modelData.value = v
            }
    }
}

Any explanations as to why and how it can be overcome would be gratefully received


Solution

  • When you activate a NavigationLink in the sidebar, the destination view gets "pushed" onto the detail column. So after you click on a navigation link, the detail view is either displaying detailPage(v: 1) or detailPage(v: 2). It is no longer displaying the original detailPage(v: modelData.value) that you put in detail: { ... }.

    Since you passed a constant (1 or 2) to the v parameter, the detailPage will always display that, regardless of what model.value is.

    The use of onChange is totally redundant. Before you click on the navigation links, the detail column is detailPage(v: modelData.value), so v is the same as modelData.value anyway. After you click on the navigation link, v is a constant that never changes, so onChange cannot possibly be triggered at all. Keep in mind that the navigation links push a new view to replace the original detail column - these are separate views, each with their own onChange detecting changes of their own vs.

    In the first place though, this is not how NavigationLinks are designed to be used in the sidebar column of a NavigationSplitView. The only documented way to use NavigationLinks in the sidebar column is "coordinate with a list". That is,

    final class ModelData: ObservableObject {
        @Published var value: Int = 1
    }
    
    struct ContentView: View {
        @StateObject var modelData = ModelData()
        var body: some View {
            NavigationSplitView{
                List(selection: $modelData.value) {
                    NavigationLink("Detail Page 1", value: 1)
                    NavigationLink("Detail Page 2", value: 2)
                }
            }detail:{
                Text("Detail Page")
                Text("\(modelData.value)").font(.largeTitle)
            }
            .environmentObject(modelData)
        }
    }