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:
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:
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.)
-
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. ↩︎
-
The implementation of
dateFormatter
isn’t shown here. It’s a global constant that provides the sameDateFormatter
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. ↩︎