0

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")
        }
    }
}
FunnyHorse
  • 33
  • 1
  • 6

1 Answers1

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.

malhal
  • 26,330
  • 7
  • 115
  • 133
  • 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? – FunnyHorse Jun 28 '23 at 07:53
  • 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. – malhal Jun 28 '23 at 08:01
  • 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. – FunnyHorse Jun 28 '23 at 08:47
  • if you use `.task` then you can put the result as values or structs in `@State` and don't need any objects. – malhal Jun 28 '23 at 08:58
  • 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`. – FunnyHorse Jun 28 '23 at 09:14
  • its better to use SwiftUI architecture where view state should be in `@State` not `@Published` if you use other architecture you'll face many problems – malhal Jun 28 '23 at 09:42