The SwiftUI Environment
Understanding SwiftUI's Environment is a mandatory step on the journey of learning SwiftUI. In this new article series, we will dive into many aspects of this core SwiftUI component.
Alongside SwiftUI's Environment, we will introduce a few concepts here and there; let's wait no further!
View Hierarchy
A View Hierarchy establishes the relationship among all views in any given window.
This hierarchy is connected by many parent-child relationships. Given any view:
- the view containing that view is known as its parent view (a.k.a. the superview)
- the views within that view are known as its children views (a.k.a. subviews)
Every view in the hierarchy has:
- (up to) one parent/superview
- zero or more children/subviews
In UIKit, we can access both superview and subviews of any
UIView
via itssuperview: UIView?
andsubviews: [UIView]
properties.
Views that share the same parent/superview are called siblings, every view can have zero or more siblings.
A tree structure
If we zoom out, a view hierarchy can be seen as an inverted tree structure, where:
- each node is a
UIView
/View
- every link is a parent-child relationship
The top-most node, also known as the root of our tree, is UIWindow
's rootViewController
's view
.
Everything we display on screen will have this root view as its ancestor.
We can print the view hierarchy at any time in
lldb
viapo UIWindow.value(forKeyPath: "keyWindow.rootViewController.view.recursiveDescription")!
A SwiftUI Example
So far, we've been talking primarily in UIKit terms: the same definitions apply to SwiftUI.
Let's define a small SwiftUI view:
struct FSView: View {
var body: some View {
VStack {
HStack {
ViewA()
ViewB()
}
ViewC()
}
}
}
Everything we define in our view body
is a descendant of FSView
. This is the complete FSView
view hierarchy:
FSView
└── VStack
├── HStack
│ ├── ViewA
│ └── ViewB
└── ViewC
Where:
FSView
is the root of our view hierarchyVStack
is the only child ofFSView
HStack
andViewC
are siblings andVStack
's childrenViewA
andViewB
are siblings andHStack
's children
If we define our app WindowGroup
with FSView
, the view hierarchy above is our complete view hierarchy.
@main
struct FSApp: App {
var body: some Scene {
WindowGroup {
FSView() // 👈🏻 SwiftUI view hierarchy starts here.
}
}
}
We can think of SwiftUI's
WindowGroup
definition as equivalent to UIKit'sUIWindow.rootViewController.view
SwiftUI Environment
SwiftUI environment is a collection of values and objects propagated through the view hierarchy.
A lot of what we do in SwiftUI affects the environment. A few examples of environment values/objects:
- every view style
- accent, tint, foreground colors
onSubmit(of:_:)
'sTriggerSubmitAction
,openURL
's action- the app AppDelegate and SceneDelegate
- ...and much more
Values vs Objects
When we talk about environment values, we refer to all definitions under EnvironmentValues
:
these are mainly primitives such as an enum (e.g., the colorScheme
value for dark and light mode) and other value-type instances, such as booleans (e.g., isEnabled
) and numbers (e.g., Text
's lineLimit
).
On the contrary, environment objects are classes conforming to ObservableObject
. These are the same ObservableObject
s we define and use via @StateObject
or @ObservedObject
, with the difference that, instead of passing them down via view initializers, we can fetch them via environment.
Setting an environment value/object
There are mainly two ways to set values/objects into the environment:
- Directly, via the
environment(_:_:)
andenvironmentObject(_:)
view modifiers:
FSView()
.environment(\.colorScheme, .dark) // Setting an environment value
.environmentObject(appState) // Setting an environment object
Learn about SwiftUI's App-wide state here.
- Indirectly, via convenience view modifiers:
FSView()
.foregroundColor(.yellow)
.buttonStyle(.borderedProminent)
These are also examples of environment values that are not part of
EnvironmentValues
's public API (FB8161189), but we can still set them via these modifiers.
The effect is the same, and it's up to the API designer to decide which way to take.
Reading an environment value/object
Regardless of how we set them, the only way to read both environment values and objects is via SwiftUI's associated property wrappers:
struct FSView: View {
@EnvironmentObject var state: AppWideState // Reading an environment object
@Environment(\.isEnabled) private var isEnabled: Bool // Reading an environment value
var body: some View {
...
}
}
Learn about SwiftUI's App-wide state here.
Note that accessing to an environment object that is not present in the environment will terminate the app.
Environment propagation
Environment values/objects are propagated through the view hierarchy. When we set something into the environment, we set it for:
- the view where the change is applied to
- the view's hierarchy descendants
Let's set the foregroundColor
to red (🔴) in our FSView
example:
struct FSView: View {
var body: some View {
VStack {
HStack {
ViewA()
ViewB()
}
ViewC()
}
.foregroundColor(.red) // 🔴
}
}
As we're setting it for VStack
, both VStack
and its descendants will inherit this foreground color value:
FSView
└── VStack 🔴
├── HStack 🔴
│ ├── ViewA 🔴
│ └── ViewB 🔴
└── ViewC 🔴
An environment value/object is propagated until that same value/object is set again, let's set the foregroundColor
to green (🟢) on the HStack
:
struct FSView: View {
var body: some View {
VStack {
HStack {
ViewA()
ViewB()
}
.foregroundColor(.green) // 🟢
ViewC()
}
.foregroundColor(.red) // 🔴
}
}
Now only VStack
and ViewC
will see red for their foregroundColor
, while others will see green:
FSView
└── VStack 🔴
├── HStack 🟢
│ ├── ViewA 🟢
│ └── ViewB 🟢
└── ViewC 🔴
Note how siblings can have different environment values:HStack
and ViewC
are siblings but, for the same environment value, the former sees green, while the latter sees red.
What about FSView
?
So far we have changed the environment to FSView
's body
. If we want to change the environment seen by FSView
, we will then need to do so before its declaration, for example:
@main
struct FSApp: App {
var body: some Scene {
WindowGroup {
FSView()
.foregroundColor(.purple) // 🟣
}
}
}
At this point, FSView
inherits purple as its foregroundColor
. However, its children will see what has been set for them:
FSView 🟣
└── VStack 🔴
├── HStack 🟢
│ ├── ViewA 🟢
│ └── ViewB 🟢
└── ViewC 🔴
If we didn't set the foregroundColor
in FSView
's body
, its descendants would also see purple.
Environment default values
All environment values (EnvironmentValues
) come with a default value (nil
or something more appropriate): if no value is explicitly set, SwiftUI will use that default value.
Continuing with foregroundColor
as an example, if we don't set it ourselves, SwiftUI will use black in light mode, and white in dark mode. These are the two default values defined by the SwiftUI team for that particular environment value.
This is how/why, by default,
Text
and all shapes will be rendered with black and white in, respectively, light and dark mode.
On the contrary, there's no default value for environment objects. These are ObservableObject
classes that we define and set: SwiftUI doesn't initialize nor hold those objects for us. Except for a few cases like app and scene delegates, we are responsible to initialize and inject them into the environment when/where appropriate.
Conclusions
Whether we realize it or not, the environment is undoubtedly one of the most used aspects of any SwiftUI app.
SwiftUI views and controls are expected to adapt to their context and presentation, and the environment plays a significant role in making this possible.
In the following article in the series, we will continue our exploration with more advanced uses and some edge cases to be aware of. Make sure to subscribe via feed RSS or follow @FiveStarsBlog
on Twitter.
Thank you for reading!