3

I faced the problem when NavTestChildView called more one times. I don't understand what going wrong. I tested on a real device with iOS 16.0.3 and emulator Xcode 14.0.1

I replaced original code to give more info about the architecture why I create NavTestService into navigationDestination.

enum NavTestRoute: Hashable {
    case child(Int)
}

class NavTestService: ObservableObject {
    let num: Int
    
    init(num: Int) {
        self.num = num
        print("[init][NavTestService]")
    }

    deinit {
        print("[deinit][NavTestService]")
    }
}

struct NavTestChildView: View {
    @EnvironmentObject var service: NavTestService

    init() {
        print("[init][NavTestChildView]")
    }

    var body: some View {
        Text("NavTestChildView \(service.num)")
    }
}

struct NavTestMainView2: View {
    var body: some View {
        VStack {
            ForEach(1..<10, id: \.self) { num in
                NavigationLink(value: NavTestRoute.child(num)) {
                    Text("Open child \(num)")
                }
            }
        }
    }
}

struct NavTestMainView: View {
    var body: some View {
        NavigationStack {
            NavTestMainView2()
                .navigationDestination(for: NavTestRoute.self) { route in
                    switch route {
                    case let .child(num):
                        NavTestChildView().environmentObject(NavTestService(num: num))
                    }
                }
        }
    }
}

logs:

[init][NavTestChildView]
[init][NavTestService]
[deinit][NavTestService]
[init][NavTestChildView]
[init][NavTestService]

2 Answers 2

1

Looks like there is a period when instance of NavTestService is not held by anyone and it leaves the heap. In practice this would hardly ever happen because .environmentObject vars are usually held somewhere up the hierarchy. If you change NavTestMainView accordingly:

struct NavTestMainView: View {
    let navTestService = NavTestService()
    var body: some View {
        NavigationStack {
            NavigationLink(value: NavTestRoute.child) {
                Text("Open child")
            }
            .navigationDestination(for: NavTestRoute.self) { route in
                switch route {
                case .child:
                    NavTestChildView().environmentObject(navTestService)
                }
            }
        }
    }
}

... you get no deinits and no extra init as well. The console will output:

[init()][NavTestService]
[init()][NavTestChildView]
[init()][NavTestChildView]

Also note that if you comment out let navTestService = NavTestService() and wrap NavTestChildView().environmentObject(NavTestService()) in LazyView you'll get the following output:

[init()][NavTestChildView]
[init()][NavTestService]

Where LazyView is:

struct LazyView<Content: View>: View {
    let build: () -> Content
    init(_ build: @autoclosure @escaping () -> Content) {
        self.build = build
    }
    var body: Content {
        build()
    }
}
7
  • 1
    Is this related to my question here? stackoverflow.com/questions/73978107/…
    – Andre
    Oct 13, 2022 at 18:21
  • 1
    I've managed to reproduce your issue, @Andre. If I find and answer, I'll give it.
    – Paul B
    Oct 13, 2022 at 18:43
  • In my case I can't create NavTestService before users click on a link cause I don't know all parameters Oct 14, 2022 at 8:08
  • 1
    The service needs to either be a singleton or a @stateobject
    – malhal
    Oct 14, 2022 at 11:44
  • You are right @malhal. Generally we use .environmentObject() to share the state between views, so navTestService would likely be @StateObject IRL. However making it @StateObject here makes no difference.
    – Paul B
    Oct 14, 2022 at 13:50
0

It's not "firing" it's just initing the View struct multiple times which is perfectly normal and practically zero overhead because View structs are value types. It tends to happen because UIKit's event driven design doesn't align well with SwiftUI's state driven design.

You can simplify your code by replacing the router enum / case statement with multiple navigationDestination for each model type.

6
  • In this case SwiftUI will make instances of NavTestService when NavTestMainView2 is called. NavigationLink(value: NavTestService(num: 1)) { Text("Open child \(num)") } Oct 14, 2022 at 14:33
  • We aren't supposed to init objects in View structs
    – malhal
    Oct 14, 2022 at 19:18
  • Why? Where should we init objects? Oct 15, 2022 at 7:38
  • Because the hierarchy of view structs is created, used and destroyed on every state change. If you init an object on the heap during this time then it’s considered a memory leak and performance hit. That’s why you have to use the property wrapper @stateobject when you need a reference type for state however in your case I don’t think you need a reference type.
    – malhal
    Oct 15, 2022 at 7:46
  • You wrote about using a model type instead of enum I thought about NavTestService. Could you give an example to clear our conversation? Oct 15, 2022 at 12:25

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.