EnvironmentObject And ObservableObject
When your view structure is complex and has multiple layers of nesting, using @EnvironmentObject
allows you to share state across the entire view hierarchy without having to manually pass ObservableObject at each layer. However, you don’t need to write @EnvironmentObject var viewModel: AppViewModel at each layer. You only need to declare it in the view that really needs to access the ViewModel.
Suppose you have a multi-layer nested view structure as follows:
1
2
3
4
5
6
|
ContentView
├── LeftView
│ └── SubLeftView (does not need viewModel)
├── MiddleView (does not need viewModel)
└── RightView
└── SubRightView (need viewModel)
|
In this example, SubRightView needs access to AppViewModel, but the middle layers MiddleView and SubLeftView do not. Therefore, you do not need to declare @EnvironmentObject on these middle layers, only in SubRightView(and LeftView if necessary).
Let’s take an example of a macOS app with a three-column layout: left, middle, and right views. Only the right view’s child (sub-view) needs access to a shared AppViewModel
, an ObservableObject
that tracks the selected option. Here’s how you can manage this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
import SwiftUI
// 1. Define an ObservableObject to manage app state
class AppViewModel: ObservableObject {
@Published var selectedOption: String? = nil
}
struct ContentView: View {
@StateObject var viewModel = AppViewModel() // Initialize the ViewModel
var body: some View {
HStack(spacing: 0) {
// Left view
LeftView()
.frame(width: 200)
// Middle view, does not need access to the ViewModel
MiddleView()
.frame(width: 200)
// Right view
RightView()
.frame(maxWidth: .infinity)
}
.environmentObject(viewModel) // Inject ViewModel into the environment
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct LeftView: View {
var body: some View {
VStack {
Text("Left View")
SubLeftView() // Sub-view does not need ViewModel
}
}
}
// SubLeftView does not need the ViewModel
struct SubLeftView: View {
var body: some View {
Text("Sub Left View")
}
}
struct MiddleView: View {
var body: some View {
Text("Middle View")
}
}
struct RightView: View {
var body: some View {
VStack {
Text("Right View")
SubRightView() // Sub-view that needs the ViewModel
}
}
}
// SubRightView needs access to the ViewModel
struct SubRightView: View {
@EnvironmentObject var viewModel: AppViewModel // Access the ViewModel here
var body: some View {
VStack {
if let option = viewModel.selectedOption {
Text("You selected: \(option)")
} else {
Text("No option selected")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray.opacity(0.1)) // Background color for the right view
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
|
Explanation of the Code
-
AppViewModel
:
This is an ObservableObject
that holds the shared state (selectedOption
). It’s marked with @Published
to notify SwiftUI to update views when this value changes.
-
ContentView
:
The root view uses @StateObject
to instantiate the AppViewModel
and injects it into the environment using .environmentObject(viewModel)
. This makes viewModel
available to all views within this view hierarchy.
-
LeftView
and SubLeftView
:
These views do not need access to AppViewModel
, so there’s no need to declare @EnvironmentObject
here. They simply display static text.
-
MiddleView
:
Like the left views, this middle view does not need access to the shared state, so there’s no @EnvironmentObject
here either.
-
RightView
:
The right view contains a sub-view (`S