Custom environment values are useful when a whole part of the view tree needs the same small piece of configuration or dependency.

The annoying part has always been the setup code.

For a long time, this was the normal shape:

struct AnalyticsClient {
    var track: (String) -> Void

    static let disabled = AnalyticsClient { _ in }
}

private struct AnalyticsClientKey: EnvironmentKey {
    static let defaultValue = AnalyticsClient.disabled
}

extension EnvironmentValues {
    var analyticsClient: AnalyticsClient {
        get { self[AnalyticsClientKey.self] }
        set { self[AnalyticsClientKey.self] = newValue }
    }
}

That still works, but most of the code is just scaffolding around one stored value.

SwiftUI’s @Entry macro lets the declaration say the same thing directly:

extension EnvironmentValues {
    @Entry var analyticsClient: AnalyticsClient = .disabled
}

That is the whole custom environment value. The macro creates the storage key and the accessor that EnvironmentValues needs. The public API your views use is still the key path, \.analyticsClient.

You read it the same way as before:

struct ProductRow: View {
    let product: Product

    @Environment(\.analyticsClient) private var analyticsClient

    var body: some View {
        Button(product.title) {
            analyticsClient.track("product_opened")
        }
    }
}

And you inject the real value from the part of the app that owns it:

ProductListView()
    .environment(\.analyticsClient, analyticsClient)

The useful bit is that the environment value now takes up roughly the amount of code it deserves.

Adding a View Modifier

I usually still add a small view modifier for values that are part of my app’s own UI vocabulary:

extension View {
    func analyticsClient(_ analyticsClient: AnalyticsClient) -> some View {
        environment(\.analyticsClient, analyticsClient)
    }
}

That gives the call site a nicer shape:

ProductListView()
    .analyticsClient(analyticsClient)

This is not required, but it is often worth doing for values that appear in previews, feature roots, or tests. It also mirrors SwiftUI’s own style: you normally write .lineLimit(2), not .environment(\.lineLimit, 2).

Why I Prefer @Entry

The main benefit is not that it saves a few lines. The benefit is that the important part becomes visible.

With the old version, every custom value needs a key type, a default value, a getter, and a setter. In code review, I do not want to spend attention checking whether the getter and setter point at the same key type.

With @Entry, the declaration says exactly what matters:

extension EnvironmentValues {
    @Entry var isPaywallEnabled = false
    @Entry var analyticsClient: AnalyticsClient = .disabled
    @Entry var imageCache: ImageCache = .shared
}

That is easier to scan. It is also harder to accidentally copy the wrong key name from another environment value.

I like it most for app-level values that are simple by design:

  • feature flags used by a group of views
  • lightweight clients or closures for actions
  • display configuration for a component family
  • app or feature dependencies with a sensible preview or test default

The old pattern made these values feel heavier than they really were. @Entry puts them closer to the amount of code they deserve.

The Default Still Matters

The one part I would not hide from myself is the default value.

An environment default should be cheap, safe, and boring. A disabled analytics client is fine. A default value that opens a database, starts work, or creates app state is a smell.

If the default needs a little explanation, I would give it a name:

extension AnalyticsClient {
    static let preview = AnalyticsClient { event in
        print("Preview event:", event)
    }
}

extension EnvironmentValues {
    @Entry var analyticsClient: AnalyticsClient = .preview
}

That keeps the @Entry declaration small without turning the default into a mystery.

The Trade-Offs

The trade-off is that a macro hides generated code. In this case I think that is a good default, but it is still a trade-off.

When I see this:

@Entry var analyticsClient: AnalyticsClient = .disabled

I know SwiftUI is generating the key type for me, but I am no longer naming that key myself. For most app code, that is fine. The key type was an implementation detail anyway.

The other practical trade-off is tooling. @Entry is a SwiftUI macro, so the module has to be built with a toolchain and SDK that understand it. If a package or app still needs to build with older Xcode versions, the explicit EnvironmentKey version is the safer choice.

That is separate from deployment target. Because the macro expands at compile time, the generated code can support older OS versions as long as the APIs used by your value and surrounding code are available there. The macro may be fine for an older deployment target, while the specific example around it still uses newer SwiftUI APIs.

When I Would Still Use the Old Pattern

The main reason I would keep the old pattern is custom accessor behavior.

For example, if I wanted to clamp a value before it enters the environment, I would keep the explicit key:

private struct RefreshIntervalKey: EnvironmentKey {
    static let defaultValue: TimeInterval = 30
}

extension EnvironmentValues {
    var refreshInterval: TimeInterval {
        get { self[RefreshIntervalKey.self] }
        set { self[RefreshIntervalKey.self] = max(5, newValue) }
    }
}

That extra code is doing real work. The setter documents a rule, and the rule is enforced at the boundary where the value enters the environment.

I would also keep the old pattern when the code has to build with a toolchain that does not support @Entry.

That’s about it. For ordinary custom environment values, @Entry should be the default. Reach for the longer EnvironmentKey version when the accessor needs to do something special, or when the project still has to build with an older toolchain.