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)")
}
}
5
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 NavigationLink
s/navigationDestination
.
A NavigationLink
inside NavigationStack
that is inside NavigationSplitView
presents in its own column.
The injection should always happen at the uppermost shared View
.
9
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.
9
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
1