2

I am building a navigation system based on NavigationStack and modifiers .navigationDestination().

What I am trying to achieve: I need to build a View hierarchy with navigation:

- ContentView
  - ViewA.navigationDestination()
    - ViewB.navigationDestination()
      - ...

Each level of view hierarchy can contains it's own .navigationDestination() to handle navigation events. Navigation event is triggered by appending NavigationPath of top level NavigationStack. In WWDC 2022 video about navigation it is confirmed that we can use nested navigationDestination modifiers.

The problem: when button "Down to B" pressed, I can see ViewB() is pushed. But also I see @StateObject StoreB() is initialized, then deinitialized, and then initialized again.

Question: Why this code triggering multiple initialization of StateObject StoreB? Is it normal behaviour or it's a SwiftUI bug?

I tried to place navigationDestination //B right after //A, and there is no repeated init. I can go this way, but I like the idea of describing navigation in place where it's belong.

Can you suggest some way to work around this multiple init?

struct ContentView: View {
    @State var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            ViewA(path: $path)
        }
    }
}

// MARK: - View A
struct ViewA: View {
    @Binding var path: NavigationPath

    var body: some View {
        Button("Down to B") {
            path.append(RouteToB())
        }
        .navigationDestination(for: RouteToB.self) { route in   //A
                ViewB(path: $path)
        }
    }

    struct RouteToB: Hashable { }
}

// MARK: - View B
struct ViewB: View {
    @Binding var path: NavigationPath
    @StateObject var storeB = StoreB()

    var body: some View {
        Button("Down to C") {
            path.append(RouteToC())
        }
        .navigationDestination(for: RouteToC.self) { route in   //B
            Color.green
        }
    }

    struct RouteToC: Hashable { }

    class StoreB: ObservableObject {
        private let value = 8

        init() {
            print("+++ Store B inited")
        }

        deinit {
            print("--- Store B deinited")
        }
    }
}

1 Answer 1

0

.navigationDestination(for:) should be the value or object you want to pass along, not a route.

@StateObject is for when you want a reference type in an @State, i.e. for something async or a Combine pipeline with lifetime tied to view. We don't really need it anymore since we have .task(id:) and can store the result in values or structs using @State.

If your object isn't doing anything async then change it to @State. You can group related @State vars into a custom struct and use mutating func for logic. If the object is for model data you would be better with a singleton so it is never deinit and pass into every View using .environmentObject(Store.shared) in the App struct. That way you can also use .environmentObject(Store.preview) with sample data for previews and benefit from faster design.

Sign up to request clarification or add additional context in comments.

11 Comments

Thank you for answering. I have a specific value passed to navigationDestination, triggering showing of specific view, so I am a little confused. Could you please expand your thoughts? In real app I have @StateObject with @Published properties, dispatcher for actions and so on, I don't really imaging how to put all of this functionality into .task(). Anyway, why do you think described code example triggered double init? Is it something I should investigate or just accept and move on?
If your object isn't doing anything async then change to @State. You can group related @State vars into a custom struct and use mutating func for logic. If the object is for model data you would be better with a singleton so it is never deinit and pass into every View using .environmentObject(Store.shared) in the App struct. That way you can also use .environmentObject(Store.preview) with sample data for previewing.
My object is doing async work. The reason why I trying to escape using .enviromentObject is because it would be a lot of nested views with it's own StateObjects, doing async work and managing view state, so I will end up with a bunch of StateObjects, initialized on app launch.
if you use .task then you can put the result as values or structs in @State and don't need any objects.
I got you, but I can't use .task that way. Project has UDF architecture, all view logic is isolated from a view inside of StateObject, view only sees a @Published var viewState.
|

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.