SwiftUI Navigation Stack pops back on ObservableObject update

I have a starting view that contains the StateObject created from an Observable Object.

Code Block language
struct PostLoginView: View {
  @StateObject var userLoader = FirestoreUser(uid: Auth.auth().currentUser!.uid, getUpdates: true)
var body: some View {
    TabView(selection: $currentTab) {
      NavigationView{
        UserProfileView(currentTab: $currentTab, userLoader: userLoader, postsLoader: postsLoader)
        .environmentObject(userSettings)
      }

This gets passed to other views where it is observed.

Code Block language
struct UserProfileView: View {
  @ObservedObject var userLoader: FirestoreUser


Code Block language
Group{
            HStack {
              Spacer()
              NavigationLink(destination: ProfilePhotoUpdateView(userLoader: self.userLoader).environmentObject(self.userSettings)) {
                Image(systemName: "pencil").padding(.horizontal).font(Font.title.weight(.bold))
                  .foregroundColor(buttonStartColor)
              }
            }


This chain can continue for a few more links

If the model userLoader is updated in any way, the stack pops back automatically to the first view. This is not the desired behavior. How can I keep the current child view when the model is updated?


Having the same issue, and it's a real nusance. Is there some sort of use pattern to avoid this.?
I ran into the same issue, which seems to be because when a dataupdate is published from an ObservableObject, the view listening for it will re-render, and therefore tear down the viewstack that sits on top of it. It would be nice to have an apple-provided pause/resume publish functionality of the ObservableObject in order to allow a view down in the viewstack to update its datasource in the background without tearing down the viewstack on top of it. I have not found such functionality, but implemented something that so far handles it quite nicely.

This is particularly relevant when you have views that is built upon several datasources, so that one part of the view is ready for further interaction and navigation while other parts of the view is still loading, and I do not want wherever I navigated to to pop just because another part of the parent-view datasources finished loading.

The way I handled it was:
In my ObservableObject, I removed the @Published keyword for the variables in question in order to avoid the auto-publishing of updates.
In addition I added a private "visible" var, and functions for onAppear and onDisappear which sets the visibility variable accordingly.

Whenever my data should be loaded/updated, I check the visibility variable in order to determine if changes are to be published. If yes, then call objectWillChange.send() which will publish that there is a change in the observable object, and prepare the view owning the observable to fetch new data from it.
Now, in that view, I added .onAppear and .onDisappear, where I call the onAppear and onDisappear of the observable.

In the onAppear of the ObservableObject, I also call the objectWillChange.send() in order to trigger a publish of the data inside.

With this approach, I am able to allow an ObservableObject to refresh its datasource in the background while another view is pushed on top of it without popping the viewstack when data is loaded. But as soon as the view in question once again becomes visible, it will be populated with fresh data.
Update: Made a small extention that so far looks to be handling it. Not thoroughly tested though, so might need some rework. But the general idea is

Extend ObservableObject

Code Block language
class PausableObservableObject: ObservableObject {
private var isPaused: Bool = false
private var hasPendingUpdates = false
var onResume: (() -> ())? = nil
func publishWillUpdate() {
if (!isPaused) {
self.objectWillChange.send()
} else {
hasPendingUpdates = true
}
}
func pause() {
isPaused = true
}
func resume() {
isPaused = false
if hasPendingUpdates {
hasPendingUpdates = false
self.objectWillChange.send()
}
onResume?()
}
}


And make a viewModifier that is visibility aware:
Code Block language
struct VisibilityAwareObservables: ViewModifier {
let observables: [PausableObservableObject]
func body(content: Content) -> some View {
AnyView(content)
.onAppear {
for observable in observables {
observable.resume()
}
}
.onDisappear {
for observable in observables {
observable.pause()
}
}
}
}
extension View {
func visibilityAwareObservables(observables: [PausableObservableObject]) -> some View {
ModifiedContent(content: self, modifier: VisibilityAwareObservables(observables: observables))
}
}


Now make your observableObject an instance of PausableObservableObject and make sure that whenever data to be published is changed, you call
Code Block language
publishWillUpdate()


and simply pass this object along with any other PausableObservableObject into the viewModifier visibilityAwareObservables

Example:

Code Block language
struct ExampleView: View {
@ObservedObject var datasource = MyDatasourceObservable()
/* MyDatasourceObservable extends PausableObservableObject */
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(datasource.somePublishedParameter)
}
.visibilityAwareObservables(observables: [datasource])
}
}




Having the same issue on iOS 15, before updating it was working fine (on iOS 14.5), no popping back to the parent view

I fixed the issue by using a button with a navigation link instead of just NavigationLink and now it doesn't pop to the root view when the parent's state changes. Here is the code:

struct CustomNavigationLink<Content, Destination>: View where Destination: View, Content: View {
  let destination: Destination?
  let content: () -> Content

  @State private var isActive = false

  init(destination: Destination, @ViewBuilder content: @escaping () -> Content) {
    self.content = content
    self.destination = destination
  }

  var body: some View {
    ZStack {
      NavigationLink(destination: destination, isActive: $isActive) {}

      Button(action: {
        self.isActive = true
      }) {
        content()
      }
    }
  }
}

This is a bug that kicked with v14.5 and is still there several months later with version 15.1. @Apple - Can you let us know this is on your radar?

Changing the NavigationView's style to .stack seems to do the trick for me. This has solved other navigation-related problems in the past for me as well.

You need to apply the modifier directly to the NavigationView, not to its contents:

NavigationView {
    ShoppingListsView()
}.navigationViewStyle(.stack) 
16

isDetailLink(false) on the NavigationLink will fix this problem

On iOS 15.1.1 this works.

NavigationLink(
  destination: List{
    // result
    // ForEach(){ item in ...
  }
)
.isDetailLink(false)

The new NavigationStack in SwiftUI for iOS 16 doesn't seem to have this issue anymore.

having the same issue with snap back however, my app snaps back from a nav link when a textfield on the nav link view is selected. none of the above worked for me.

What a weird and annoying issue this is, wasted a day and nothing seem to work.

This is because your observable object is being used by both a view that is hosting a navigation view, and one of the children being presented by a navigation view.

So when the child updates the observable object, it causes the parent to reload. Which in turn causes its navigation stack to be drawn from scratch as well.

the solution is to decouple the parent from the observable object. So it’s not reloading each time it changes.

A technique I use is to wrap an observable object inside another. What this does it it allows me to inject them via @EnvironmentObject, without causing the views to reload unnecessarily.

class Dependency<T: ObservableObject>: ObservableObject {
    let observable: T

    init(observable: T) {
        self.observable = observable
    }
}

In case anyone else having a similar problem. Using following with NavigationStack solved the problem for me. As explained in these documents,

https://developer.apple.com/documentation/swiftui/navigationlink

https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types

NavigationLink {
    FolderDetail(id: workFolder.id)
} label: {
    Label("Work Folder", systemImage: "folder")
}

change NavigationView { content } to NavigationStack { content }

SwiftUI Navigation Stack pops back on ObservableObject update
 
 
Q