1

Consider code

@EnvironmentObject var navModel: NavigationModel
var body: some View {
    someView
       .navigationDestination(for: ImageModel.self) { imageModel in
                ImageDetailedView(image: imageModel)
                    .environmentObject(navModel)   //this is required 
       }
}

Is navigation not considered a child of the view? And if so, is it normal to keep throwing around environemntObjects around the navigation stack?

import Combine
import SwiftUI

enum Destination {
    case firstPage
    case secondPage
}

enum Category: Int, Hashable, CaseIterable, Identifiable, Codable {
    case dessert
    case pancake
    case salad
    case sandwich
    
    var id: Int { rawValue }
    
    var localizedName: LocalizedStringKey {
        switch self {
        case .dessert:
            return "Dessert"
        case .pancake:
            return "Pancake"
        case .salad:
            return "Salad"
        case .sandwich:
            return "Sandwich"
        }
    }
}

@available(iOS 16.0, *)
class Coordinator3: ObservableObject {
    @Published var path = NavigationPath()
    
    func gotoHomePage() {
        path.removeLast(path.count)
    }
    
    func tapOnEnter() {
        path.append(Destination.firstPage)
    }
    
    func tapOnFirstPage() {
        path.append(Destination.secondPage)
    }
    
    func tapOnSecondPage() {
        path.removeLast()
    }
    
    func test() {
        path.removeLast(path.count)
        path.append(2)
    }
}

class Test :ObservableObject {
    var name = "test"
}

@available(iOS 16.0, *)
struct SplitTestView: View {
    @State var selectedCategory: Category?
    var categories = Category.allCases
    @ObservedObject var coordinator = Coordinator3()
    @StateObject var test = Test()
    
    var body: some View {
        NavigationSplitView {
            List(categories, selection: $selectedCategory) { category in
                NavigationLink(category.localizedName, value: category)
            }
        } detail: {
            NavigationStack(path: $coordinator.path) {
                switch selectedCategory {
                case .dessert:
                    Text(selectedCategory!.localizedName)
                case .pancake:
                    VStack {
                        Text("Navigation stack")
                            .padding()
                        NavigationLink("NavigationLink to enter first page", value: Destination.firstPage)
                            .padding()
                        NavigationLink("NavigationLink to enter second page", value: Destination.secondPage)
                            .padding()
                    }
                    .navigationDestination(for: Destination.self) { destination in
                        if destination == .firstPage {
                            FirstPage()
                        } else {
                            Text(
                                "SecondPage()"
                            )
                        }
                    }
                case .salad: Text(selectedCategory!.localizedName)
                case .sandwich: Text(selectedCategory!.localizedName)
                case .none: Text("hi")
                }
            }.environmentObject(test)
        }
    }
}

@available(iOS 16.0, *)
struct SplitTestView_Previews: PreviewProvider {
    static var previews: some View {
        SplitTestView()
    }
}
struct FirstPage: View {
    @EnvironmentObject var test: Test
    var body: some View {
        Text("First Page \(test.name)")
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
erotsppa
  • 14,248
  • 33
  • 123
  • 181
  • You haven’t provided a reproducible example but more than likely 1. You didn’t inject on the stack, not within it has to be on, 2. You haven’t injected in the view before this screen 3. The source of truth isn’t a StateObject. – lorem ipsum Mar 16 '23 at 09:59
  • @loremipsum nope apparently it's a known problem for at least 2 years. Injection does not work across Navigation https://stackoverflow.com/questions/59812640/environmentobject-doesnt-work-well-through-navigationlink – erotsppa Mar 24 '23 at 05:40
  • @loremipsum stop giving out incorrect answers if you dont know the subject. You've been flagged. The answer is clearly stated by Apple here https://developer.apple.com/forums/thread/683564 – erotsppa Mar 24 '23 at 14:43
  • `NavigationStack{/*other stuff that includes a navigationDestination*/}.environmentObject(navModel)` will 100% work if you have `@StateObject var navModel: NavigationModel = NavigationModel()` in the same `View` with the Stack. I have no doubt about this. Notice that the injection is **on** the closing bracket. You can report all you want, Apple did not confirm this is a bug in that link. – lorem ipsum Mar 24 '23 at 15:54
  • @loremipsum sorry it just doesn't post a project that shows it otherwise – erotsppa Mar 24 '23 at 16:41

3 Answers3

1

This is why MREs are important that is why I mentioned it in my first comment, you introduced NavigationSplitView.

Scenario 1

If you are using NavigationSplitView you have to inject the EnvironmentObject to the NavigationSplitView.

NavigationSplitView{
    /*other stuff that includes a navigationDestination*/
}.environmentObject(navModel)

Scenerio 2

When working with just NavigationStack you have to inject on the NavigationStack

NavigationStack{
    /*other stuff that includes a navigationDestination*/
}.environmentObject(navModel)

Scenerio 3 - Deprecated

When working with just NavigationView you have to inject on the NavigationView

NavigationView{
    /*other stuff that includes a NavigationLink*/
}.environmentObject(navModel)

Your Sample

Just move the injection code one line down.

import Combine
import SwiftUI

@available(iOS 16.0, *)
struct SplitTestView: View {
    @State var selectedCategory: Category?
    var categories = Category.allCases
    @StateObject var coordinator = Coordinator3() //<-- Switch to StateObject
    @StateObject var test = Test()
    
    var body: some View {
        NavigationSplitView {
            List(categories, selection: $selectedCategory) { category in
                NavigationLink(category.localizedName, value: category)
            }
        } detail: {
            NavigationStack(path: $coordinator.path) {
                switch selectedCategory {
                case .dessert:
                    Text(selectedCategory!.localizedName)
                case .pancake:
                    VStack {
                        Text("Navigation stack")
                            .padding()
                        NavigationLink("NavigationLink to enter first page", value: Destination.firstPage)
                            .padding()
                        NavigationLink("NavigationLink to enter second page", value: Destination.secondPage)
                            .padding()
                    }
                    .navigationDestination(for: Destination.self) { destination in
                        if destination == .firstPage {
                            FirstPage()
                        } else {
                            Text(
                                "SecondPage()"
                            )
                        }
                    }
                case .salad: Text(selectedCategory!.localizedName)
                case .sandwich: Text(selectedCategory!.localizedName)
                case .none: Text("hi")
                }
            }
        }.environmentObject(test) //<<--- Add to the NavigationSplitView - The NavigationLink's are presenting in a separate column than the Stack, the only thing they share is the split view.
    }
}
enum Destination {
    case firstPage
    case secondPage
}

enum Category: Int, Hashable, CaseIterable, Identifiable, Codable {
    case dessert
    case pancake
    case salad
    case sandwich
    
    var id: Int { rawValue }
    
    var localizedName: LocalizedStringKey {
        switch self {
        case .dessert:
            return "Dessert"
        case .pancake:
            return "Pancake"
        case .salad:
            return "Salad"
        case .sandwich:
            return "Sandwich"
        }
    }
}

@available(iOS 16.0, *)
class Coordinator3: ObservableObject {
    @Published var path = NavigationPath()
    
    func gotoHomePage() {
        path.removeLast(path.count)
    }
    
    func tapOnEnter() {
        path.append(Destination.firstPage)
    }
    
    func tapOnFirstPage() {
        path.append(Destination.secondPage)
    }
    
    func tapOnSecondPage() {
        path.removeLast()
    }
    
    func test() {
        path.removeLast(path.count)
        path.append(2)
    }
}

class Test :ObservableObject {
    var name = "test"
}



@available(iOS 16.0, *)
struct SplitTestView_Previews: PreviewProvider {
    static var previews: some View {
        SplitTestView()
    }
}
struct FirstPage: View {
    @EnvironmentObject var test: Test
    var body: some View {
        Text("First Page \(test.name)")
    }
}

Addl Info

In the Migration Guide apple talks about the differences between the 2 types.

https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types

They call the inside of the NavigationStack "content"

NavigationStack {
    /* content */
}

And the inside of the NavigationSplitView "columns"

NavigationSplitView {
    /* column 1 */
} content: {
    /* column 2 */
} detail: {
    /* column 3 */
}

In their respective setups the "columns" and "content" only share the NavigationSplitView or NavigationStack respectively with the NavigationLinks/navigationDestination.

A NavigationLink inside NavigationStack that is inside NavigationSplitView presents in its own column.

The injection should always happen at the uppermost shared View.

lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
  • Ok you are correct, I have accepted the answer. But I still don't understand why injecting it at the NavigationStack level fails, but for some reason, a level above NavigationSplitView would work. The idea is that you inject it so that all subviews would have access. So A->B->C. If you inject at A it works, but if you inject at B it fails? Weird. – erotsppa Mar 26 '23 at 02:28
  • 1
    @erotsppa according to your link that is “Apple’s design”. I look at it like B and C are eggs in a carton. A is the carton. The eggs are connected via the carton. They can’t share with each other but the carton can. Idk maybe it’s a silly analogy. – lorem ipsum Mar 26 '23 at 02:34
  • But that's not the case here. Let's explore the tree. A (NavigationSplitView), B (NavigationStack), C1 (VStack), C2 (view inserted by navigationDestination). Ok so we know that C2 crashes, if you inject at B. But it works if you inject at A. According to documentation, all subviews will have access to inserted EO. So that MUST mean that C1 and C2 are not subviews of NavigationStack, but instead somehow subviews of NavigationSplitView. Ok I can understand that maybe NavigationStack was not never part of the hierarchy since it's a "column", then how does it become a child view of A? – erotsppa Mar 27 '23 at 04:18
  • @erotsppa only Apple can answer that. Maybe submit a feedback – lorem ipsum Mar 27 '23 at 09:23
-1

It is a known behaviour that environmentObjects do not flow through the navigation system. You have to do manual injections every time.

Additionally in the WWDC lounges someone asked:

I’ve had several intermittent crashes from environment objects being nil when I pass them to a sheet or NavigationLink. [...]

The answer was:

NavigationLink by design doesn’t flow EnvironmentObjects through to its destination as it’s unclear where the environmentObject should be inherited from. I suspect this might what’s causing your issue. In order to get the behavior you expect, you’ll have to explicitly pass the environmentObject through at that point.

https://developer.apple.com/forums/thread/683564

erotsppa
  • 14,248
  • 33
  • 123
  • 181
  • 1
    Like the comment says this is not a bug it is design, if you inject in the upper most `View` that is shared such a `NavigationStack`, `NavigationView`, `NavigationSplitView` or `TabVIew`, etc. then all the `NavigationLink`s will have access to the `EnvironmentObject` because it isn't the link sharing the object it is the upper most `View`. MREs important for context. – lorem ipsum Mar 25 '23 at 13:33
-1

I've found that as my projects get more complex, using EnvironmentObjects get a bit buggy. Something I've been incorperating is shared singleton classes instead of EnvironmentObjects classes.

To do this, inside of your class, list a property static let shared = NavigationModel(). Then, in any view you want to use that class, reference it in the view properties as @ObservedObject var navModel = NavigationModel.shared.

There are pros and cons to this approach, but I've found that the pros outweigh the cons as environment objects tend to be buggy.

sadel
  • 31
  • 7
  • 1
    I tend to agree. Unless anyone else have found any downside. I'm not even sure why they invented environmentObject – erotsppa Mar 24 '23 at 16:37
  • 1
    This is a workaround that demonstrates a misunderstanding of how EnvironmentObject works. For it to pass to subviews such as NavigationLinks you have to inject **on** the stack. – lorem ipsum Mar 24 '23 at 17:49
  • @loremipsum please provide a minimum working example demonstrating this – erotsppa Mar 24 '23 at 22:05
  • 1
    @erotsppa you already have the answer, you can add it to the marked answer if you want to. You didn’t provide an MRE to start all of this would have been over with a long time ago if you provided an MRE – lorem ipsum Mar 24 '23 at 22:28
  • 1
    @loremipsum just to proof you wrong, here you go MRE https://github.com/dpyy/TestInjection.git – erotsppa Mar 25 '23 at 06:21
  • look at the answer I posted – lorem ipsum Mar 25 '23 at 13:13
  • @loremipsum I understand you need to inject the env obj on the stack, I was just saying I've had legitamite SwiftUI bugs working with them in the past – sadel Mar 25 '23 at 21:19
  • @loremipsum whats the advantage of using environmentObject over just a global? – erotsppa Mar 25 '23 at 22:28
  • 1
    @erotsppa global as in this solution you lose the ability to change your environment mocks, previews, etc. it is also a preference thing there is a ton of debate on singletons online, I personally try to only use them when dealing with delegates. I like using Dependency Injection. – lorem ipsum Mar 25 '23 at 22:39