NavigationStack with NavigationPath triggers multiple init/deinit of views in stack

Hi all,

I've noticed some weird behavior when working NavigationStack paired with a NavigationPath, and I'm wondering if it's by design or perhaps a bug.

In short: I'm experiencing that every time I push a new view to the NavigationPath, all the previous views appear to init and deinit, which can cause all sorts of problems if you aren't aware of it happening. It's seems like .navigationDestination(for: ) is run once per item in the path that is given to the NavigationStack. So if you add 3 items it'll run 3 times when adding the third view. But the original views and their state are kept.

Is this happening because pushing a view to the stack is seen as a state change? And is it intended?

The longer explanation: So I'm developing an app in pure SwiftUI and I'm trying to establish a way of navigating through a router / coordinator. I like that my ViewModels can determine when navigation should happen and not the view. E.g. Normally I'd like to prepare some sort of data that should be transferred to the next view.

I've prepared an example project which you can use to check out the issue. It's not a full example of my setup, but it's small enough to show what I'm experiencing. It can be found here: https://github.com/Kimnyhuus/NavigationStackDemo

The structure is:

  • Router
  • App
    • RootView
      • AppleView + AppleViewModel
      • BananaView + BananaViewModel
      • PearView + PearViewModel

So the Router is an ObservableObject that contains a @Published NavigationPath object + functions for adding / removing to / from stack.

I've also added an enum here which defines the destinations that the Router can take an navigate to.

RootView is setup with a NavigationStack which is setup with the NavigationPath in the parameter:

NavigationStack(path: $router.navPath) { ... }

RootView also have the router setup as an EnvironmentObject:

.environmentObject(router)

This enables the other views to interact with the router and push new views to the stack.

Each view is initialized with its corresponding VM. The VMs contain nothing other than init and deinit, a variable containing the initialized id + a @Published num which can be set from the view. This is to keep track of the instances in the prints to the console. Each view can navigate to the two other views.

You can try and run the project yourselves, but I've made an example of the inits/deinits that happens here.

First, I'm navigating from RootView -> AppleView which is expected. The router prints from func navigate(to destination: Destination) that a view has been pushed to the stack. The RootView prints, when .navigationDestination(for: Destination.self) { ...} is triggered, and it says we're navigating to .apple. And then we see that the AppleVM is inited. All like expected.

||| Router: add to navPath: 1
||| NavStack: Destination .apple
||| Init ☀️: AppleViewModel, id: 879, num: 0

Then I navigate from AppleView -> BananaView and the weird stuff starts happening. We see that a second view has been added to the stack. BananaVM is inited like we'd expect. But then the previous actions seem to run again but with new instances.

||| Router: add to navPath: 2
||| NavStack: Destination .banana
||| Init ☀️: BananaViewModel, id: 167, num: 0
||| NavStack: Destination .apple
||| Init ☀️: AppleViewModel, id: 492, num: 0

Then I navigate from BananaView -> PearView and it's continuing. It's now clear that .navigationDestination(for: Destination.self) { ... } is run once per item in the stack.

||| Router: add to navPath: 3
||| NavStack: Destination .pear
||| Init ☀️: PearViewModel, id: 436, num: 0
||| NavStack: Destination .banana
||| Init ☀️: BananaViewModel, id: 292, num: 0
||| NavStack: Destination .apple
||| Init ☀️: AppleViewModel, id: 434, num: 0
||| Deinit 🔥: AppleViewModel, id: 492, num: 0

Finally I navigate from PearView to AppleView and it's just piling on.

||| Router: add to navPath: 4
||| NavStack: Destination .apple
||| Init ☀️: AppleViewModel, id: 738, num: 0
||| NavStack: Destination .pear
||| Init ☀️: PearViewModel, id: 564, num: 0
||| NavStack: Destination .banana
||| Init ☀️: BananaViewModel, id: 769, num: 0
||| Deinit 🔥: BananaViewModel, id: 292, num: 0
||| NavStack: Destination .apple
||| Init ☀️: AppleViewModel, id: 283, num: 0
||| Deinit 🔥: AppleViewModel, id: 434, num: 0

Navigating back towards the RootView, you can see that it again inits and deinits different instances of the view models. You’ll notice the original ones with state being deinit’ed where it has a number higher than 0 in “num”.

||| Router: rm navPath: 3
||| NavStack: Destination .banana
||| Init ☀️: BananaViewModel, id: 222, num: 0
||| Deinit 🔥: BananaViewModel, id: 769, num: 0
||| NavStack: Destination .pear
||| Init ☀️: PearViewModel, id: 801, num: 0
||| Deinit 🔥: PearViewModel, id: 564, num: 0
||| NavStack: Destination .apple
||| Init ☀️: AppleViewModel, id: 173, num: 0
||| Deinit 🔥: AppleViewModel, id: 283, num: 0
||| Deinit 🔥: AppleViewModel, id: 738, num: 0

||| Router: rm navPath: 2
||| NavStack: Destination .banana
||| Init ☀️: BananaViewModel, id: 26, num: 0
||| Deinit 🔥: BananaViewModel, id: 222, num: 0
||| NavStack: Destination .apple
||| Init ☀️: AppleViewModel, id: 744, num: 0
||| Deinit 🔥: AppleViewModel, id: 173, num: 0
||| Deinit 🔥: PearViewModel, id: 801, num: 0
||| Deinit 🔥: PearViewModel, id: 436, num: 3

||| Router: rm navPath: 1
||| NavStack: Destination .apple
||| Init ☀️: AppleViewModel, id: 401, num: 0
||| Deinit 🔥: AppleViewModel, id: 744, num: 0
||| Deinit 🔥: BananaViewModel, id: 26, num: 0
||| Deinit 🔥: BananaViewModel, id: 167, num: 2

||| Router: rm navPath: 0
||| Deinit 🔥: AppleViewModel, id: 401, num: 0
||| Deinit 🔥: AppleViewModel, id: 879, num: 1

What is making NavigationStack / .navigationDestination(for: ) run through all the previous items in the stack, every time the state changes in the NavigationPath?

I hope it's not too confusing with all the prints :-) Please let me know if I need to add more info.

Answered by Knyhuus in 794091022

I made a better example of the problem here: https://developer.apple.com/forums/thread/758772?page=1#793986022

Disregard this post.

Accepted Answer

I made a better example of the problem here: https://developer.apple.com/forums/thread/758772?page=1#793986022

Disregard this post.

NavigationStack with NavigationPath triggers multiple init/deinit of views in stack
 
 
Q