6

Description

I am trying to adapt my application to the new NavigationStack introduced in IOS 16. I ended up with a strange behaviour when I have a @StateObject variable in one of my views.

When I navigate (using the new .navigationDestination() modifier) to a new view that has a @StateObject the init() block of that object will run twice.

The body of the view looks like this:

VStack {
    Text("Param: \(intParam) and \(viewModel.someData)")
            
    Button("Do Something") {
        viewModel.buttonTapped()
    }
}

BUT

If I remove the Button element the init() of the @StateObject will only run once.

Also

If I use the older NavigationLink(title:destination:) element to navigate to the new page it will run the @StateObject init() once.

Full Code

struct ContentView: View {
    
    var body: some View {
        NavigationStack {
            VStack {
                NavigationLink(value: 16) {
                    Text("Tap Me (IOS 16)")
                }
                NavigationLink("Tap Me (Older)") {
                    Page2(intParam: 45)
                }
            }
            .navigationDestination(for: Int.self) { i in
                Page2(intParam: i)
            }
            .navigationTitle("Navigation")
        }
    }
}

struct Page2: View {
    @StateObject var viewModel = ViewModel()
    let intParam: Int
    
    init(intParam: Int) {
        self.intParam = intParam
        print("Page2 view created")
    }
    
    var body: some View {
        VStack {
            Text("Param: \(intParam) and \(viewModel.someData)")
            
            Button("Do Something") {
                viewModel.buttonTapped()
            }
        }
    }
}

extension Page2 {
    @MainActor class ViewModel: ObservableObject {
        @Published var someData = "something"
        
        init() {
            print("Page2 viewmodel created")
        }
        
        func buttonTapped() {
            print("do something")
        }
    }
}

Do you have any idea for the reason of this behaviour?

4
  • navigationDestination(for: destination:) is getting called multiple times. You can remove all the remaining code and try and file a feedback with Apple Nov 11, 2022 at 13:55
  • Very interesting, I knew that navigationDestination was called multiple times but have never seen it also initing @StateObjects which is because body is also being needlessly called. I found this other known issue on the forums where navigationDestination can be called with the wrong value developer.apple.com/forums/thread/714967
    – malhal
    Nov 11, 2022 at 17:06
  • That one is interesting as well @malhal. Actually I need to do a series of actions in when the ViewModel gets initiated. I need to start WebSocket communication with a remote server and also need to start a timer instance. But this way these actions are done twice. I thought about doing it on the .onAppear() of the view, but that gets called every time I navigate back to the page. Do you have an idea where should I do these? Nov 12, 2022 at 16:13
  • You may need to use a "defer view" - I provide one in this related SO Post: stackoverflow.com/a/75551258/5970728
    – kwiknik
    Feb 23, 2023 at 23:20

1 Answer 1

2

Not a bug, but it can certainly be confusing for developers. When using the destination method of the NavigationStack to navigate to other views, SwiftUI may indeed call the closure code inside navigationDestination multiple times (mostly two times). However, it uses the result of the last call to generate the next layer of views. This means that no matter how many StateObject instances you create, only the last generated instance will be used in the target view. The other instances will be automatically destroyed.

To be precise, this is not creating multiple instances of the same StateObject, but rather executing the operation of creating views multiple times and only keeping the result of the last creation.

As developers, we only need to know that when accessing the next layer of views, your StateObject instance is unique, and it remains unique when navigating to more layers.

This also tells developers from another perspective not to do any expensive operations in init, but to place such operations in onAppear or task.

As for the issue mentioned in the reply that onAppear may be called multiple times (when returning to the view), it can be solved by adding a @State flag.

1
  • Thanks for the reply. Haven't worked on this much for some time, but I will check it out regarding your answer and accept the it Apr 24, 2023 at 16:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.