Infinite loop using NavigationStack

Hello.

I've seen some other posts about NavigationStack, but my variation seems a little different.

FYI, I have not migrated from ObservableObject to Observable yet. However, having a path in either seems to be a factor in this issue.

My code has no issues when built with Xcode 15.

When built with Xcode 16 I keep hitting scenarios where the .onAppear for my first tab gets called over and over again endlessly.

If I go to my second tab, which uses a NavStack with a path and then navigate anywhere my .onAppear for my FIRST tab gets call endlessly. I’ll sometimes see a “double push” to the stack. (Someone posted a video of this happening on Mastodon, which apparently I’m not allowed to link to here.) The second tab is accessing the path property via an @EnvironmentObject.

I can stop this endless loop by removing @Published from the property in my ObservableObject that holds my path.

But then if I go to my third tab, which does NOT use a path, the .onAppear for my FIRST tab again gets called endlessly.

So far on Mastodon I’ve seen three people encountering problems possibly related to storing a path in something being observed.

Feedback requires a sample project, which I am having trouble creating to show the problem.

I was able to recreate the double push part of the problem so I opened feedback.

FB14270042

Some more context:

The first time I encountered this endless loop was in this scenario: App launches and brings up default tab which I call my starting view. I found that when I switched to another tab but then went back to my first tab my .onAppear for the starting view would get called endlessly.

I “fixed” this by switching the .onAppear to .task instead BUT .task ended up being called twice in this same scenario. Still not ideal but better than endless.

I figured out how to make the endless loop stop. So I now have workarounds to both scenarios.

I am able to recreate the "double push" after navigation behavior in a sample project. I am able to stop it by removing @Published from my property that holds a navigation path in my @ObservableObject.

Also, adding @ObservationIgnored to a similar property in an @Observable apparently works also.

I am NOT able to recreate the endless calling of .onAppear/.task in a sample project. I am able to stop it in my app by removing @Published from my property that holds the selected tab in my @ObservableObject.

This is code that is functioning without an issue with iOS 17. I only encountered these problems when building for iOS 18.

My celebration was short lived because I found another situation that triggers the same endless loop behavior.

I have a @Published property that controls what sheet is launched. Populating it causes this same loop. And this time removing @Published makes the sheet logic stop working.

So I don't have a workaround any more.

No change with Version 16.0 beta 5 (16A5221g).

No change with Version 16.0 beta 6 (16A5230g).

I've made multiple design changes that I hoped would solve this problem but I still have it.

My app actually no longer has a tabs. I also moved the array used for navigation from my ObservableObject to its own dedicated Observable.

The problem still happens relatively the same.

If I navigate to my Settings View (which used to be a separate tab) and then navigate into another View the infinite loops starts and won't stop.

With the iPad simulator (and my iPad) I can still do a little with the app, although it is very slow. With the iPhone simulator the CPU of the simulator goes to over 100% and locks up.

I did get this tip: "A workaround that I’ve seen for some examples is to make the view being pushed Equatable, (comforming properly to the protocol) and then do .equatable() in the destination." Unfortunately that does not seem to fix the problem in my experiments.

printChanges will show that it thinks that my ObservableObject is continuing to change:

StartingView: @self, _weatherData changed.

I'm becoming gravely concerned considering how close to Sept 9 we are. It's greatly affecting my ability to test my app and I don't feel like I could release it in this state.

I finally have an understanding of what is happening here.

Credit where credit is due because ChatGPT put me on the right path to fix this.

It said to check in .task if any of my Object's properties were being changed, EVEN WITH THE SAME VALUE.

Previously I had some computed properties that were being called too much so I compute them in a few places manually, including .task/.onAppear.

I tried adding seatbelts around those assignments to do nothing if the values weren't changing.

After doing that the loop is gone.

I'm not quite understanding why this code was fine in iOS 17 but goes off the rails in iOS 18.

Best guesses:

  • Assigning a Published property the same value in iOS 17 does NOT trigger a View update but with iOS 18 it does?
  • The order/sequence/etc of .task/.onAppear changed and it made this shortcoming of my app apparent.

This whole summer I thought my problem was related to the "double push" to the NavigationStack that was happening if your NavigationPath was in an Observable or ObservedObject. (FB14270042)

When I moved my NavigationPath to its own class and printChanges STILL reported that it thought my original object was changing constantly I realized something else was happening.

Infinite loop using NavigationStack
 
 
Q