Home

The environment

Dependency injection down the tree, and what does not belong in it

The environment is a dictionary carried down the view tree. A view writes into it, and every view below can read it without anything in between knowing it exists.

You are already using it constantly. .font(), .foregroundStyle(), .tint(), .lineLimit() and the dark-mode colour scheme are all environment values, which is why they apply to a whole subtree rather than to one view.

@Environment(\.colorScheme) private var colorScheme
@Environment(\.dynamicTypeSize) private var typeSize
@Environment(\.dismiss) private var dismiss

dismiss is worth pausing on. It is an action, injected by whatever presented this view, and the view calling it does not know whether it is in a sheet, a navigation stack or a popover. That is the environment doing its actual job: the caller states what it wants, and the context decides how.

Adding your own value

With @Entry, one line does it:

extension EnvironmentValues {
    @Entry var analytics: Analytics = .noOp
}

Then inject above and read below:

ContentView()
    .environment(\.analytics, LiveAnalytics())

// anywhere underneath
@Environment(\.analytics) private var analytics

The default value matters more than it looks. It is what previews get, what tests get, and what runs if somebody forgets the injection — so make it something harmless and silent rather than a fatalError. A no-op analytics client is right; a crash is a trap for whoever adds the next preview.

Tip

Before @Entry this needed a struct conforming to EnvironmentKey with a defaultValue, plus a computed property on EnvironmentValues. You will still see that shape in existing code — it is the same mechanism, spelled out longhand.

Observable objects in the environment

An @Observable model goes in by type rather than by key path:

ContentView()
    .environment(library)

// underneath
@Environment(Library.self) private var library

Note that the read is not optional. If nothing injected a Library, this crashes at runtime, in the view that reads it, with a message about a missing environment object. Use @Environment(Library.self) private var library: Library? where the value is genuinely optional, and be aware that every preview of every view below the injection point now needs the object too.

What does not belong in the environment

It is convenient enough to be over-used, and the failure mode is the same one global variables have: you cannot see a view’s dependencies from its signature.

Anything a view needs to function should be a parameter. If BookRow cannot render without a Book, take a Book. The environment is for things that are ambient and have a sensible default, not for the subject of the view.

Frequently changing values are a performance problem. Every view reading an environment value re-evaluates when it changes, and the reads are invisible at the call site, so a value updating sixty times a second can redraw half the app for reasons nobody can find by reading the code.

Anything you would call a service locator in another language. Two or three cross-cutting dependencies — a theme, an analytics client, a date formatter — are reasonable. Fifteen is a container, and a container in the environment means no view can be constructed in a test without building the whole world.

The rule I use: if removing the injection should be a compile error, it is a parameter. If it should silently fall back to something sensible, it is an environment value.

Reading it at the right level

The same trap as the previous chapter, in a new costume:

struct Screen: View {
    @Environment(\.dynamicTypeSize) private var typeSize   // whole screen depends on it

    var body: some View {
        VStack {
            Header()
            ExpensiveChart()
            Footnote(size: typeSize)
        }
    }
}

Every accessibility text-size change now re-evaluates Screen, and ExpensiveChart is rebuilt for a value it does not use. Move the @Environment read into Footnote, and the chart stops caring.

Environment reads are dependencies, and they are the easiest kind to place too high, because unlike a parameter they leave no trace in the call site to remind you.