SwiftUI NavigationView pops back when updating observableObject

Background: I have been stuck on this annoying problem for days, trying different solution and searching apple developer forum, stackoverflow etc for answers; some have had similar problems but no suggested solution had the desired effect.

The problem is that when updating an observableObject or environmentObject down the navigation hierarchy view stack, the views get popped back to root. Viewing data from observableObject is fine, but not editing.

Scenario is:

  1. I navigate to: root -> view1 -> view2.
  2. I update the environmentObject in View2 but then I get pushed back to: root -> view1

I have simplified my app in order to make it more understandable. See below:

ObservableObject:

class DataStore: ObservableObject {
    static let shared = dataStore() 
    @Published var name   : Int = ""
}

RootView:

struct ContentView: View {
   @StateObject var dataStore = DataStore.shared
   @State private var isShowingView1 = false
    var body: some View {
        NavigationView{
            VStack{
                Text(dataStore.name)
                NavigationLink(destination: View1(), isActive: $isShowingView1) { }
                Button(action: {
                    isShowingView1 = true
                })
            }
        }
    }
}

View1:

struct View1: View {
    @EnvironmentObject var dataStore: dataStore
    @State private var isShowingView2 = false
    var body: some View {
        ScrollView{
            VStack(alignment: .center) {
                Text(dataStore.name)
                NavigationLink(destination: View2(), isActive: $isShowingView2) { }
                Button(action: {
                    isShowingView2 = true
                }){
                    Text("Go to View2")
                }
            }
        }
    }
}

View2:

struct View2: View {
    @EnvironmentObject var dataStore: dataStore
    var body: some View {
        ScrollView{
            VStack(alignment: .center) {
                Text(dataStore.name)
                Button(action: {
                    dataStore.name = "updated value"
                }){
                    Text("Update data")
                }
                // When updating this environmentObject the viewstack will be pushed back to View1. If view2 had been navigated to view3 and the view3 had been updating the environmentObject, then it would also be pushed back to View1.
            }
        }
    }
}

Solution: I spent many hours searching for solutions and trying different approaches, but nothing I tried worked. There seemed to be a few other people that had the same problem as I experienced, but the suggested solutions didn't cut it. But then I stumbled on a solution for this problem when trying to implement another feature. So to be frank I am writing here now, not to ask this great community for help, but instead to give back to the community by providing the this solution to others that might need to see this.

The solution is really simple implement but was not so easy to come across. If you experience a problem similar to me then you will only need to update your rootView accordingly:

RootView Updated:

struct ContentView: View {
   @StateObject var dataStore = DataStore.shared
   @State private var isShowingView1 = false
    var body: some View {
        NavigationView{
            VStack{
                Text(dataStore.name)
                NavigationLink(destination: View1(), isActive: $isShowingView1) { }
                Button(action: {
                    isShowingView1 = true
                })
            }
        }
        .navigationViewStyle(.stack)
        //ADD THIS LINE ABOVE
    }
}

This one line .navigationViewStyle(.stack) fixed the problem of popping the viewstack for me. Unfortunately I can't provide you with the logic explanation for this behaviour, but it works and I am satisfied with that. Perhaps you are too, or perhaps you have insight on why this solution actually achieves the desired effect of allowing views down the hierarchy update observableObjects without being popped.

Happy coding :)

  • Yesssss. I had the exact same problem and this solved it! Thank you so much for sharing

  • thanks dear sir, battled this for the good part of 2 days before finding your post 🙏

Add a Comment

Accepted Reply

The solution is really simple implement but was not so easy to come across. If you experience a problem similar to me then you will only need to update your rootView accordingly:

RootView Updated:

struct ContentView: View {
   @StateObject var dataStore = DataStore.shared
   @State private var isShowingView1 = false
    var body: some View {
        NavigationView{
            VStack{
                Text(dataStore.name)
                NavigationLink(destination: View1(), isActive: $isShowingView1) { }
                Button(action: {
                    isShowingView1 = true
                })
            }
        }
        .navigationViewStyle(.stack)
        //ADD THIS LINE ABOVE
    }
}

This one line .navigationViewStyle(.stack) fixed the problem of popping the viewstack for me. Unfortunately I can't provide you with the logic explanation for this behaviour, but it works and I am satisfied with that.

  • thanks, I spent good few hours rearranging views, playing with scrollview items, observed objects and more thinking that something is refreshing and forcing UI to rebuild, your solution worked for me

  • Thank you for posting this, I also spent a lot of time trying to fix that and that instantly did it for me.

  • .navigationViewStyle(.stack) is deprecated. Changing NavigationView { content } to NavigationStack { content } did the trick for me.

Replies

Interesting.

There was something a bit similar where style attribute changed the behaviour of stack:

https://developer.apple.com/forums/thread/692911

The solution is really simple implement but was not so easy to come across. If you experience a problem similar to me then you will only need to update your rootView accordingly:

RootView Updated:

struct ContentView: View {
   @StateObject var dataStore = DataStore.shared
   @State private var isShowingView1 = false
    var body: some View {
        NavigationView{
            VStack{
                Text(dataStore.name)
                NavigationLink(destination: View1(), isActive: $isShowingView1) { }
                Button(action: {
                    isShowingView1 = true
                })
            }
        }
        .navigationViewStyle(.stack)
        //ADD THIS LINE ABOVE
    }
}

This one line .navigationViewStyle(.stack) fixed the problem of popping the viewstack for me. Unfortunately I can't provide you with the logic explanation for this behaviour, but it works and I am satisfied with that.

  • thanks, I spent good few hours rearranging views, playing with scrollview items, observed objects and more thinking that something is refreshing and forcing UI to rebuild, your solution worked for me

  • Thank you for posting this, I also spent a lot of time trying to fix that and that instantly did it for me.

  • .navigationViewStyle(.stack) is deprecated. Changing NavigationView { content } to NavigationStack { content } did the trick for me.

Thanks for sharing! Your solution just concluded a multi hour long search and debug for me.

Worked Perfectly. Thank you Sir.

would love a solution that would also work for iPad split-ui view...

I so desperately needed wanted this to work, but no. Stack has the iPad problem and also causes pickers using menustyle in child views to stop working.

Works a treat, thanks a lot. Of stuck on this for some hours.

Unfortunately, the solution proposed doesn't work in the same scenario on watchOS 9+

I came across the same issue, but cannot solve it by adding .navigationViewStyle(.stack).

Instead, I switched from NavigationView to NavigationStack and got the issue solved.

PS. I am working on iOS 16+ and SwiftUI 5.7

Add a Comment

I was facing the same issue when creating a third view from second and spent hours looking for solutions even chatGPT wasn't able to find on. Thanks for your Response. It worked for me.

I was also having this issue and the proposed solution solved it and lead me to a working solution for watchOS too.

Per the apple documentation we now need to use NavigationStack - here's the relevant doc.

Thx sooooooo much, God knows how many hours I've been spent on it!!!!! I debug sooooo hard, turns out NavigationSplitView makes it happen. Now I fell this great community finally better than stackoverflow for iOS coder.

I updated the code so it actually compiles. Your original example shows the children using the environmentObject, but new shows setting it.

The following code works as expected on iOS 17/Xcode 15

class DataStore: ObservableObject {
    static let shared = DataStore()
    @Published var name: String = ""
}

struct DataStoreView: View {
    @StateObject var dataStore = DataStore.shared
    @State private var isShowingView1 = false
    var body: some View {
        NavigationView {
            VStack{
                Text(dataStore.name)
                NavigationLink(destination: View1(), isActive: $isShowingView1) { }
                Button(action: {
                    isShowingView1 = true
                }) {
                    Text("Go to View1")
                }
            }
        }
        .environmentObject(dataStore) // added this here
    }
}

struct View1: View {
    @EnvironmentObject var dataStore: DataStore
    @State private var isShowingView2 = false
    var body: some View {
        ScrollView{
            VStack(alignment: .center) {
                Text(dataStore.name)
                NavigationLink(destination: View2(), isActive: $isShowingView2) { }
                Button(action: {
                    isShowingView2 = true
                }){
                    Text("Go to View2")
                }
            }
        }
    }
}

struct View2: View {
    @EnvironmentObject var dataStore: DataStore
    var body: some View {
        ScrollView{
            VStack(alignment: .center) {
                Text(dataStore.name)
                Button(action: {
                    dataStore.name = "updated value"
                }){
                    Text("Update data")
                }
                // When updating this environmentObject the viewstack will be pushed back to View1. If view2 had been navigated to view3 and the view3 had been updating the environmentObject, then it would also be pushed back to View1.
            }
        }
    }
}