Intro to Environments in SwiftUI

· 20min · software

Base Article

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:

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!