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 its superview: UIView? and subviews: [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 hierarchy
  • VStack is the only child of FSView
  • HStack and ViewC are siblings and VStack's children
  • ViewA and ViewB are siblings and HStack'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's UIWindow.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:

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 ObservableObjects 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:

  1. Directly, via the environment(_:_:) and environmentObject(_:) view modifiers:
FSView()
  .environment(\.colorScheme, .dark) // Setting an environment value
  .environmentObject(appState) // Setting an environment object

Learn about SwiftUI's App-wide state here.

  1. 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!

⭑⭑⭑⭑⭑

Further Reading

Explore SwiftUI

Browse all

Explore iOS

Browse all