Intro to Environments in SwiftUI
· 20min · software
Environment Basics
Any new view that is generated in SwiftUI will have a corresponding Environment
that is generated by the framework. This Environment
is automatically generated, so there is no configuration necessary to create it. This Environment
is an EnvironmentValues
structure that contains properties based on device characteristics, system state, or user settings. A list of these values may be found here.
You can access values in your application using the @Environment
macro. For example, if we wanted to read in the current locale, we would use the following in a view
@Environment(/.locale) var locale: Locale
To set or override values in the environment, we can use the environment(:, :)
view modifier (also known as the environment modifier)
MyView()
.environment(\.lineLimit, 2)
Inheriting Environments
Swift Views will inherit the Environment
from its parents by default. The App will have the base environment, and then child views can override the environment with view modifiers. In the following example, note the environment value for layoutDirection
is being fixed to .leftToRight
regardless of the layoutDirection
of the system. This is important because we want the layout of the buttons to be constant, regardless of language (some languages like Arabic will change the layoutDirection
to .rightToLeft
).
struct RootView {
var body: some View {
PlayerView()
.environment(\.layoutDirection, .leftToRight)
}
}
struct PlayerView: View {
var body: some View {
HStack {
Button("previous") {
}
Button("play") {
}
Button("next") {
}
}
}
}
Context Dependent Environment Values
Some environment values will only be available in specific view contexts. For example, the dismiss
environment value can be used to:
- Dismiss a modal presentation, like a sheet or a popover
- Pop the current view from a
NavigationStack
Find more information on the dismiss environment value here. Note that every environment will have more information in documentation about specific use cases.
An example of this would be
struct ContentView: View {
@State private var showModal = false
var body: some View {
Button("Show Modal") {
showModal = true
}
.sheet(isPresented: $showModal) {
ModalView()
}
}
}
struct ModalView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Text("This is a modal view")
.padding()
Button("Dismiss") {
dismiss()
}
.padding()
}
}
}
Custom Environment Keys
For situations where we may want to create custom environment keys on top of the system-wide and view-specific keys that Apple provides, we can use the following pattern
struct ItemsPerPageKey: EnvironmentKey {
static var defaultValue: Int = 10
}
extension EnvironmentValues {
var itemsPerPage: Int {
get { self[ItemsPerPageKey.self] }
set { self[ItemsPerPageKey.self] = newValue }
}
}
We first create a struct ItemsPerPageKey
that inherits from the EnvironmentKey
protocol, and set a default static value. This defaultValue
is a required property for the protocol. Swift will infer the asssociated Value
type as the type specified for the default value. In this case, the Value
type would be specified as Int
. Then, we use the key to define a new environment value property, naming it whatever we want. This can be set as an extension of the EnvironmentValues
. The Apple Documentation elaborates on this process further. Once implemented, this new environment key may be used elsewhere in the code as other environment values.
struct RelatedProductsView: View {
@Environment(\.itemsPerPage) var count
let products: [Product]
var body: some View {
ForEach(products[..<count], id: \.id) { product in
Text(product.title)
}
}
}
The @Entry Macro
In XCode 16, we now have access to the @Entry
macro that will abstract away a lot of the boilerplate of creating these custom environment keys. Consider the itemsPerPage
key that we defined above. In XCode 16, that can be simplified down to
extension EnvironmentValues {
@Entry var itemsPerPage: Int = 10
}
It can then be used in the same manner as before. Note that this is only available from XCode 16+, so make sure that your tooling supports it.
Dependency Injection via Environment
Dependency injection is a common patterns where an object receives its dependencies from an external source, rather than creating them itself. You may already be familiar with this pattern when using @ObservedObject
. The issue with always using @ObservedObject
, is that we have to explicitly pass the object through the initializer. Environments in SwiftUI provide a way to share data across multiple views without explicitly passing it through every initializer. Essentially, it acts as a global storage, accessible by views in a view hierarchy.
struct CalendarView : View {
var body: some View {
NavigationView {
List {
ForEach(self.store.sleeps) { sleep in
NavigationLink(
destination: SleepDetailsView()
.environmentObject(SleepStore(sleep: sleep))
) {
CalendarRow(sleep: sleep)
}
}
}
}.navigationBarTitle("calendar")
}
}
Note the .environmentObject(SleepStore(sleep: sleep))
line. This will inject the SleepStore
object (that must conform to the ObservableObject
protocol) into the view hierarchy. Then, within the SleepStore
object, both it and its child views shall have access to this SleepStore
object through injection through the @EnvironmentObject
property wrapper.
struct SleepDetailsView: View {
@EnvironmentObject var sleepStore: SleepStore
var body: some View {
VStack {
Text("Details for Sleep")
SleepSummaryView()
SleepGraphView()
}
}
}
struct SleepSummaryView: View {
@EnvironmentObject var sleepStore: SleepStore
var body: some View {
Text("Summary: \(sleepStore.someProperty)")
}
}
struct SleepGraphView: View {
@EnvironmentObject var sleepStore: SleepStore
var body: some View {
// Use sleepStore data to draw a graph
}
}
Environments are extremely useful concepts in SwiftUI. They allow you to access system level values and provide view-specific values and actions out of the box. Using @EnvironmentObject
, they also provide a method of dependency injection that is more streamlined than using @ObservedObject
. Take a look!