Sheets don’t inherit the environment

Updates:

  1. Luca Bernardi from the SwiftUI team confirmed that this is not the intended behavior.
  2. This behavior has been fixed in the iOS 14.0/macOS 11.0 SDKs. Sheets now do inherit their environment.

This behavior has been fixed. As of Xcode 12.0 (the iOS 14.0/macOS 11.0 SDKs), sheets do inherit their environment.


Unlike other views, modal sheets in SwiftUI do not inherit the environment from their parent.

What is the environment?

The environment is SwiftUI’s way to pass data implicitly to child views. Among other things, the environment contains app- or system-wide preferences, such as the user’s locale or the current color scheme. Essentially, you can think of the environment as a large, heterogeneous dictionary that gets passed implicitly to every view.1

Many built-in SwiftUI views take the environment into account when they draw themselves. We can take advantage of this to override a setting for all child views with a single line of code. Consider this example:

SwiftUI view displaying three rows of text, where the text views in the middle row have a larger font size
The single .font modifier overrides the font for all child views.
VStack(spacing: 8) {
  Text("Line 1")
  HStack {
    Text("Line 2")
    VStack {
      Text("Line 2a")
      Text("Line 2b")
    }
  }.font(.title)
  Text("Line 3")
}

The .font(.title) modifier on the HStack mutates the corresponding value in the current environment, which then gets passed to the stack’s child views. And because Text grabs its font from the environment, all text views in this section of the view tree are rendered with a larger font size. Note that the modified environment also applies to indirect children of the HStack.

Example with a sheet

The following example creates a root view whose locale and dynamic type size have been overridden in the environment. The view displays a formatted date and the current dynamic type size setting. Here’s the code for the root view2:

struct RootView: View {
  var body: some View {
    RootViewContent()
      .environment(\.sizeCategory, .accessibilityMedium)
      .environment(\.locale, Locale(identifier: "ja_JP"))
  }
}

struct RootViewContent: View {
  @State var isPresentingSheet = false
  @Environment(\.sizeCategory) var sizeCategory

  var body: some View {
    VStack(spacing: 16) {
      Text("Root View").font(.title)
      Text("\(Date(), formatter: dateFormatter)")
      Text("Size category: \(String(describing: sizeCategory))")
      Button("Open Sheet") {
        self.isPresentingSheet = true
      }
    }
    .sheet(isPresented: self.$isPresentingSheet) {
      ChildView()
    }
  }
}

Tapping the button in the root view sets a state variable that triggers the presentation of a modal sheet, using the .sheet modifier:

    // …
    .sheet(isPresented: self.$isPresentingSheet) {
      ChildView()
    }
    // …

The view that gets presented displays the same data as the root view, without modifying the environment in any way:

struct ChildView: View {
  @Environment(\.sizeCategory) var sizeCategory

  var body: some View {
    VStack(spacing: 16) {
      Text("Child View").font(.title)
      Text("\(Date(), formatter: dateFormatter)")
      Text("Size category: \(String(describing: sizeCategory))")
    }
  }
}

Sheets start with a fresh environment

I’d expect that the presented view inherits the environment from the presenting view (some modifications notwithstanding since the presentation mode is also stored in the environment), but that’s clearly not the case; while the root view correctly uses the Japanese locale and a very large dynamic type setting, the child view goes back to the system locale and text size:

Screenshots of the presenting view displaying large text and a Japanese-formatted date, and the presented view displaying normal text and an English-formatted date.
The presenting view didn’t pass its environment to the presented view.

I’m not sure if this is intentional or a bug. (Update: It’s a bug.) I guess if you see sheets as independent entities that should not be affected by their presenting view, it makes sense for some environment values not to get propagated. Examples of this kind might include:

For other settings, such as locale and dynamic type size, not propagating seems the wrong choice to me. It looks like there is no single option that works for everything, though. And making this a configurable behavior that every EnvironmentKey can decide for itself could also be confusing.

Propagating environment values manually

If you want to propagate an environment value to a sheet, you must do so manually. In our example, the code would look like this:

    // …
    .sheet(isPresented: self.$isPresentingSheet) {
      ChildView()
        .environment(\.sizeCategory, self.sizeCategory)      
        .environment(\.locale, self.locale)
    }
    // …

(This assumes that you also added a property @Environment(\.locale) var locale to the root view in order to access the current locale inside the environment.)

  1. Check out the documentation for the EnvironmentValues struct for a list of all documented environment values SwiftUI ships with.

    And Chris Eidhof shows how to print out the full contents of a view’s environment↩︎

  2. The implementation of dateFormatter isn’t shown here. It’s a global constant that provides the same DateFormatter instance to all views. SwiftUI appears to read the current locale from environment and set it on the date formatter before formatting a date using the custom string interpolation syntax. ↩︎