@Entry Macro for Custom Environment Values: Eliminating Boilerplate in SwiftUI
Adding a single custom value to SwiftUI’s environment used to require an EnvironmentKey struct, a default value, and a
computed property on EnvironmentValues — three declarations just to pass one piece of data down the view hierarchy.
Multiply that by the five or ten custom values a production app typically needs, and you end up with a file full of
near-identical scaffolding that nobody wants to maintain.
In this post, you will learn how the @Entry macro collapses that ceremony into a single line, how the macro expands
under the hood, and when you should still reach for the manual approach. We will not cover building custom macros with
SwiftSyntax — that is its own deep topic covered in Swift Macros.
Contents
- The Problem
- Introducing
@Entry - How the Macro Expands
- Advanced Usage
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Suppose you are building a Pixar movie catalog app and you want to inject a MovieRatingService through the environment
so every view in the hierarchy can access it without manual parameter drilling. Before iOS 18, you had to write all of
this:
import SwiftUI
// 1. Define a key with a default value
struct MovieRatingServiceKey: EnvironmentKey {
static let defaultValue: MovieRatingService = .shared
}
// 2. Extend EnvironmentValues with a computed property
extension EnvironmentValues {
var movieRatingService: MovieRatingService {
get { self[MovieRatingServiceKey.self] }
set { self[MovieRatingServiceKey.self] = newValue }
}
}
// 3. Now you can finally use it
struct MovieDetailView: View {
@Environment(\.movieRatingService) private var ratingService
var body: some View {
Text(ratingService.rating(for: "Toy Story"))
}
}
That is 15 lines of boilerplate before you even get to the view that consumes the value. The EnvironmentKey
conformance and the get/set pair are pure ceremony — they follow the exact same pattern every single time. Worse,
each new environment value doubles the chance of a copy-paste mistake: wrong key type in the subscript, mismatched
property name, forgotten setter.
In a real codebase with values for theming, networking, feature flags, and analytics, this boilerplate adds up fast. And every new team member who encounters it for the first time has the same reaction: “Why is this so verbose?”
Introducing @Entry
Starting with iOS 18, macOS 15, watchOS 11, and visionOS 2, the
@Entry macro lets you declare a custom environment
value in a single line:
import SwiftUI
extension EnvironmentValues {
@Entry var movieRatingService: MovieRatingService = .shared
}
That is it. One line replaces the key struct, the getter, and the setter. The consumption side stays identical:
struct MovieDetailView: View {
@Environment(\.movieRatingService) private var ratingService
var body: some View {
Text(ratingService.rating(for: "Toy Story"))
}
}
And injecting a custom value from a parent view works the same way it always has:
struct PixarCatalogApp: App {
var body: some Scene {
WindowGroup {
MovieListView()
.environment(\.movieRatingService, MovieRatingService.premium)
}
}
}
Note: The
@Entrymacro was introduced in WWDC 2024 (session 10152 — “What’s new in SwiftUI”). It ships with Xcode 16+ and requires the deployment targets listed above.
Beyond EnvironmentValues
The @Entry macro is not limited to EnvironmentValues. It works with three additional container types:
// FocusValues -- custom focus-based state
extension FocusValues {
@Entry var isSearchFieldFocused: Bool = false
}
// Transaction -- custom animation/transaction metadata
extension Transaction {
@Entry var isBatchUpdate: Bool = false
}
// ContainerValues -- custom container-child communication (iOS 18+)
extension ContainerValues {
@Entry var cardStyle: CardStyle = .standard
}
The pattern is identical in each case: declare a stored property with a default value inside an extension, prefix it
with @Entry, and the macro generates the rest.
How the Macro Expands
Understanding what the macro produces helps you debug issues and reason about behavior. You can expand any macro in Xcode by right-clicking the annotation and selecting Expand Macro.
Given this declaration:
extension EnvironmentValues {
@Entry var movieRatingService: MovieRatingService = .shared
}
The macro expands to something equivalent to:
extension EnvironmentValues {
var movieRatingService: MovieRatingService {
get {
self[__Key_movieRatingService.self]
}
set {
self[__Key_movieRatingService.self] = newValue
}
}
private struct __Key_movieRatingService: EnvironmentKey {
static let defaultValue: MovieRatingService = .shared
}
}
The generated code is exactly the manual pattern you have been writing by hand — a private key struct nested inside the
extension, a static defaultValue, and a computed property with get/set forwarding to the subscript. The macro is
not doing anything magical at runtime; it is purely a compile-time code generation step.
Tip: If you ever need to inspect the generated key name for debugging (e.g., when reading SwiftUI’s internal error messages), it follows the
__Key_<propertyName>convention.
Advanced Usage
Custom Types with Default Values
The default value expression in @Entry is evaluated lazily on first access, just like a manual
EnvironmentKey.defaultValue. This means you can use factory methods or initializers without worrying about eager
allocation:
extension EnvironmentValues {
@Entry var assetLoader: PixarAssetLoader = PixarAssetLoader(
baseURL: URL(string: "https://api.example.com/assets")!,
cachePolicy: .returnCacheDataElseLoad
)
}
The PixarAssetLoader instance is not created until a view first reads \.assetLoader from the environment.
Protocol Types and Existentials
You can use protocol types as the value type, which is critical for testability and dependency injection:
protocol MovieRepository: Sendable {
func fetchMovies() async throws -> [Movie]
}
struct PixarMovieRepository: MovieRepository {
func fetchMovies() async throws -> [Movie] {
// Production implementation
[]
}
}
extension EnvironmentValues {
@Entry var movieRepository: any MovieRepository = PixarMovieRepository()
}
This lets you swap in a mock during previews or tests:
#Preview {
MovieListView()
.environment(\.movieRepository, MockMovieRepository())
}
Combining @Entry with @Observable
A common pattern is injecting @Observable objects through the environment. @Entry handles this cleanly:
@Observable
final class ToyBoxInventory {
var toys: [Toy] = []
var selectedToy: Toy?
func addToy(_ toy: Toy) {
toys.append(toy)
}
}
extension EnvironmentValues {
@Entry var toyBoxInventory: ToyBoxInventory = ToyBoxInventory()
}
Any view reading \.toyBoxInventory will automatically re-render when the observed properties change, because SwiftUI
tracks observation at the property level through the @Observable macro.
Warning: Be careful with the default value when using
@Observableclasses. Every view that reads the environment value without an explicit.environment(\.toyBoxInventory, inventory)ancestor will share the same default instance. If that is not what you want, consider making the type optional and defaulting tonil.
Optional Environment Values
When there is no sensible default, use an optional type:
extension EnvironmentValues {
@Entry var currentDirector: Director? = nil
}
Consuming views can then handle the absence explicitly:
struct MovieCreditsView: View {
@Environment(\.currentDirector) private var director
var body: some View {
if let director {
Text("Directed by \(director.name)")
} else {
Text("Director unknown")
}
}
}
Performance Considerations
The @Entry macro generates the same code you would write by hand, so there is no runtime overhead from the macro
itself. However, there are a few environment-specific performance points worth keeping in mind:
Environment propagation is O(depth). When you call .environment(\.someValue, newValue), SwiftUI stores the
override in the view’s environment dictionary. Child views inherit it by walking up the hierarchy. This is fast for
typical view trees, but if you find yourself setting dozens of environment values at the root of a deeply nested
hierarchy, consider grouping related values into a single struct:
struct PixarTheme: Sendable {
var primaryColor: Color = .blue
var fontStyle: Font = .body
var cornerRadius: CGFloat = 12
}
extension EnvironmentValues {
@Entry var pixarTheme: PixarTheme = PixarTheme()
}
One environment entry carrying a struct with three fields is cheaper than three separate entries, because SwiftUI performs fewer dictionary lookups on each access.
Default values are not free. If your default value involves expensive initialization (opening a database, allocating
a large buffer), make sure you understand that it will be created when any view reads the key without an explicit
override. Using an optional with a nil default avoids this entirely.
Apple Docs:
EnvironmentValues— SwiftUI
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Simple value or service injection | Use @Entry. Idiomatic starting with iOS 18. |
| Need to support iOS 17 or earlier | Use manual EnvironmentKey. @Entry is unavailable. |
Injecting @Observable objects | Works well. Prefer .environment(object) for single-type injection. |
| Complex default value logic | @Entry supports expressions, but manual static var is more flexible. |
Custom FocusValues or Transaction | Use @Entry. Same syntax, same benefits. |
| Macros disabled in your project | Use the manual pattern for auditability. |
Tip: If your deployment target is iOS 18+ and you have no specific reason to avoid macros, default to
@Entryfor every new environment value. You can always expand the macro to see the generated code if you need to customize it later.
@Entry vs. @Environment(ObjectType.self)
iOS 17 introduced a second way to inject @Observable objects: passing them by type rather than by key path.
// Type-based injection (iOS 17+)
.environment(ratingService)
// Key-path injection (traditional)
.environment(\.movieRatingService, ratingService)
Type-based injection does not require an EnvironmentKey at all — you just call .environment(object) and read it
with @Environment(MovieRatingService.self). This is convenient when you have exactly one instance of a type in the
environment. But when you need multiple values of the same type (e.g., a primary and secondary MovieRepository),
key-path injection with @Entry gives you distinct keys for each.
Summary
- The
@Entrymacro reduces custom environment value declarations from three components (key struct, getter, setter) to a single annotated property. - It works with
EnvironmentValues,FocusValues,Transaction, andContainerValues. - Under the hood, the macro generates the same
EnvironmentKeyconformance you would write manually — zero runtime overhead. - Use protocol types for testability and optional types when no sensible default exists.
- Group related values into a single struct to minimize environment dictionary lookups.
- Stick with the manual
EnvironmentKeypattern if you need to support deployment targets before iOS 18.
For a broader look at how environment values fit into SwiftUI’s state management story, read
SwiftUI Data Flow Patterns. And if @Entry sparked your curiosity about
what else Swift macros can automate, Building Custom Property Wrappers
explores another angle on reducing repetitive code.