ObservableObject and EnvironmentObject in View of SwiftUI

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

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy