iosswiftswiftuiswiftui-navigationlinkswiftui-navigationview

SwiftUI Handling EnvironmentObject Resets with navigationLink : Avoiding Navigation View Rebuilds


I have the code below where I use a NavigationLink to add multiple child views on button click. However, as soon as I increase the EnvironmentObject value, all views reset, and only ChildView 1 remains.

ChildView Navigation

@main
struct NavigationResetIssueApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(Observer())
        }
    }
}

enum Route {
    case child
}

class Observer: ObservableObject {
    @Published var observedValue: Int = 0
    init() {
        print("Observer init")
    }
}
struct ContentView: View {
    @EnvironmentObject var observer: Observer
    @State var navigateToRoute: Route?
    var body: some View {
        NavigationView {
            VStack {
                Text("Current observer Value: \(observer.observedValue)")
                    .font(.headline)
                    .padding()
                
                Button("Move to Child View") {
                    navigateToRoute = .child
                }
                .padding()
                
                NavigationLink(destination:  ChildView(num: 1)                    ,
                               tag: Route.child,
                               selection: $navigateToRoute,
                               label: { })
            }
        }
    }
}


struct ChildView: View {
    @EnvironmentObject var observer: Observer
    @State var viewNum:Int
    @State var navigateToRoute: Route?
    init(num:Int) {
        self.viewNum = num
        print("initializing ChildView")
    }
    var body: some View {
        VStack {
            let _ = print("chilChildViewd body")
            Text("observer Value: \(observer.observedValue)")
                .font(.headline)
                .padding()
            Button("Incrtease Observer Value") {
                observer.observedValue += 1
            }
            .padding()
            
            Button("Move to Next Child View") {
                navigateToRoute = .child
            }
            .padding()
            
            NavigationLink(destination: ChildView(num: self.viewNum+1) ,
                           tag: Route.child,
                           selection: $navigateToRoute,
                           label: { })
        }
        .navigationTitle("ChildView # \(viewNum)")
    }
}

In another place, I removed the use of EnvironmentObject in the ChildView controller and changed the EnvironmentObject value after 5 seconds in the content view. Again, all child views reset, and only the first child view remains. Here is the updated code:

struct ContentView: View {
    @EnvironmentObject var observer: Observer
    @State var navigateToRoute: Route?
    var body: some View {
        NavigationView {
            VStack {
                Text("Current observer Value: \(observer.observedValue)")
                    .font(.headline)
                    .padding()
                
                Button("Move to Child View") {
                    navigateToRoute = .child
                }
                .padding()
                
                NavigationLink(destination:  ChildView(num: 1)                    ,
                               tag: Route.child,
                               selection: $navigateToRoute,
                               label: { })
            }
        }.onAppear(perform: {
            DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: {
                print("------- ContentView After 5 sec -------")
                observer.observedValue = 50
            })
        })
    }
}


struct ChildView: View {
    @State var viewNum:Int
    @State var navigateToRoute: Route?
    init(num:Int) {
        self.viewNum = num
        print("initializing ChildView")
    }
    var body: some View {
        VStack {
            let _ = print("chilChildViewd body")
             Button("Move to Next Child View") {
                navigateToRoute = .child
            }
            .padding()
            
            NavigationLink(destination: ChildView(num: self.viewNum+1) ,
                           tag: Route.child,
                           selection: $navigateToRoute,
                           label: { })
        }
        .navigationTitle("ChildView # \(viewNum)")
    }
}

Kindly let me know how to prevent this behavior. Thank you so much for your attention and participation.


Solution

  • In addition to moving the Observer out to a @State var I think you got the navigation all mixed up. Try the NavigationStack it is far more simple.

    e.g.:

    struct ContentView: View {
        @EnvironmentObject var observer: Observer
        // here we store the navigation
        @State var path: [Int] = []
        
        var body: some View {
            NavigationStack(path: $path) {
                VStack {
                    Text("Current observer Value: \(observer.observedValue)")
                        .font(.headline)
                        .padding()
                    
                    Button("Move to Child View") {
                        path.append(1)
                    }
                    .navigationDestination(for: Int.self, destination: { num in
                        // the value appended to path will appear here as num
                        // pass the path on as binding, so we can manipulate it from the childview
                        ChildView(num: num, path: $path)
                            .environmentObject(observer)
                    })
                    .padding()
                }
            }
        }
    }
    
    
    struct ChildView: View {
        @EnvironmentObject var observer: Observer
        @Binding var path: [Int]
        // no state needed here
        var viewNum: Int
        
        init(num: Int, path: Binding<[Int]>) {
            self.viewNum = num
            self._path = path
            print("initializing ChildView")
        }
        
        var body: some View {
            VStack {
                let _ = print("chilChildViewd body")
                Text("observer Value: \(observer.observedValue)")
                    .font(.headline)
                    .padding()
                Button("Incrtease Observer Value") {
                    observer.observedValue += 1
                }
                .padding()
                
                Button("Move to Next Child View") {
                    // increase the num that gets passed to the next child
                    path.append(viewNum + 1)
                }
                .padding()
            }
            .navigationTitle("ChildView # \(viewNum)")
        }
    }
    

    Edit:

    You never mentioned this being below IOS 16, but if you really want, or need to use NavigationViewtry the example below. Tested on Xcode 15.4 and IOS 15 as target.

    struct ContentView: View {
        @EnvironmentObject private var observer: Observer
        var body: some View {
            NavigationView{
                VStack {
                    Text("Current observer Value: \(observer.observedValue)")
                        .font(.headline)
                        .padding()
                    
                    NavigationLink {
                        ChildView(num: 1)
                    } label: {
                        Text("Move to Child View")
                    }
    
                }
            }
        }
    }
    
    
    struct ChildView: View {
        @EnvironmentObject private var observer: Observer
        var viewNum: Int
        
        init(num: Int) {
            self.viewNum = num
            print("initializing ChildView")
        }
        
        var body: some View {
            VStack {
                let _ = print("chilChildViewd body")
                Text("observer Value: \(observer.observedValue)")
                    .font(.headline)
                    .padding()
                Button("Incrtease Observer Value") {
                    observer.observedValue += 1
                }
                .padding()
                
                NavigationLink {
                    ChildView(num: viewNum + 1)
                } label: {
                    Text("Move to Next Child View")
                }
            }
            .navigationTitle("ChildView # \(viewNum)")
        }
    }