From apple-dev
SwiftUI Environment and Preferences system including custom EnvironmentKey, EnvironmentValues, @Environment, preference keys, and the preference-environment data flow. Use when passing data through the view hierarchy.
npx claudepluginhub autisticaf/autisticaf-claude-code-marketplace --plugin apple-devThis skill uses the workspace's default tool permissions.
> **First step:** Tell the user: "swiftui-environment skill loaded."
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
First step: Tell the user: "swiftui-environment skill loaded."
Data propagation through the SwiftUI view hierarchy. Environment flows data downward from parent to child. Preferences flow data upward from child to parent.
Use this skill when the user:
EnvironmentKey or extend EnvironmentValues@Environment, .environment(), or environment injectionPreferenceKey, .onPreferenceChange, or .overlayPreferenceValueGeometryReader with preference-based measurement@Entry macro for simplified environment keys (iOS 18+)How should data flow through the hierarchy?
|
+- Parent provides value to all descendants (downward)
| |
| +- Value is view-scoped (theme, locale, services)
| | +- Use Environment
| |
| +- Value is two-way (parent and child both read/write)
| +- Use @Binding
|
+- Child communicates a value to ancestors (upward)
| |
| +- Child reports a measurement (size, position, anchor)
| | +- Use PreferenceKey
| |
| +- Ancestor renders an overlay/background based on child data
| +- Use .overlayPreferenceValue / .backgroundPreferenceValue
|
+- Value only needed by immediate child
| +- Pass via init parameter
|
+- Value needed by deeply nested descendants
+- Use Environment (avoids prop drilling)
| API | Minimum Version | Notes |
|---|---|---|
@Environment(\.key) | iOS 13 | Read environment values |
.environment(\.key, value) | iOS 13 | Inject environment values |
EnvironmentKey protocol | iOS 13 | Custom environment keys |
PreferenceKey protocol | iOS 13 | Child-to-parent data flow |
.onPreferenceChange | iOS 13 | React to preference updates |
.overlayPreferenceValue | iOS 13 | Overlay using preference data |
.backgroundPreferenceValue | iOS 13 | Background using preference data |
.transformEnvironment | iOS 13 | Mutate environment in-place |
@Environment(SomeType.self) | iOS 17 | Observable environment objects |
@Entry macro | iOS 18 | Simplified EnvironmentKey definition |
| Key Path | Type | Purpose |
|---|---|---|
\.colorScheme | ColorScheme | Light or dark appearance |
\.dismiss | DismissAction | Dismiss current presentation |
\.openURL | OpenURLAction | Open a URL |
\.locale | Locale | Current locale |
\.horizontalSizeClass | UserInterfaceSizeClass? | Compact or regular width |
\.verticalSizeClass | UserInterfaceSizeClass? | Compact or regular height |
\.isEnabled | Bool | Whether the view is enabled |
\.editMode | Binding<EditMode>? | List edit mode |
\.managedObjectContext | NSManagedObjectContext | Core Data context |
\.openWindow | OpenWindowAction | Open a new window (macOS/iPadOS) |
\.requestReview | RequestReviewAction | Request App Store review |
struct ThemeKey: EnvironmentKey {
static let defaultValue: Theme = .standard
}
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
struct ProfileView: View {
@Environment(\.theme) private var theme
var body: some View {
Text("Hello")
.foregroundStyle(theme.primaryColor)
}
}
ContentView()
.environment(\.theme, .dark)
The @Entry macro eliminates the boilerplate of defining a separate key struct:
extension EnvironmentValues {
@Entry var theme: Theme = .standard
}
This single line replaces the EnvironmentKey struct and the computed property. Reading and injection use the same @Environment(\.theme) and .environment(\.theme, value) syntax.
protocol NetworkService: Sendable {
func fetch(_ url: URL) async throws -> Data
}
extension EnvironmentValues {
@Entry var networkService: any NetworkService = URLSessionNetworkService()
}
struct UserListView: View {
@Environment(\.networkService) private var network
var body: some View {
List { /* ... */ }
.task { let data = try? await network.fetch(usersURL) }
}
}
// Swap for previews or tests
#Preview {
UserListView()
.environment(\.networkService, MockNetworkService())
}
Modify an inherited environment value in-place rather than replacing it:
MyView()
.transformEnvironment(\.theme) { theme in
theme.cornerRadius = 16
}
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
Common reduce strategies: max(value, nextValue()) for largest wins, value + nextValue() for totals, value = nextValue() for last writer wins, value.append(contentsOf: nextValue()) for collections.
// Child reports its width via a background GeometryReader
Text("Hello")
.background(
GeometryReader { proxy in
Color.clear
.preference(key: WidthPreferenceKey.self, value: proxy.size.width)
}
)
// Ancestor reads the aggregated preference
.onPreferenceChange(WidthPreferenceKey.self) { width in
maxChildWidth = width
}
Use .anchorPreference to report anchors, then .overlayPreferenceValue to render decorations:
struct BoundsPreferenceKey: PreferenceKey {
static var defaultValue: Anchor<CGRect>? = nil
static func reduce(value: inout Anchor<CGRect>?, nextValue: () -> Anchor<CGRect>?) {
value = value ?? nextValue()
}
}
// Child reports its bounds
Text("Important")
.anchorPreference(key: BoundsPreferenceKey.self, value: .bounds) { $0 }
// Ancestor draws overlay using the anchor
.overlayPreferenceValue(BoundsPreferenceKey.self) { anchor in
if let anchor {
GeometryReader { proxy in
let rect = proxy[anchor]
RoundedRectangle(cornerRadius: 8)
.stroke(.blue, lineWidth: 2)
.frame(width: rect.width, height: rect.height)
.offset(x: rect.minX, y: rect.minY)
}
}
}
.environment(\.theme, .dark)
|
v (Environment flows DOWN)
ParentView
/ \
ChildA ChildB
|
GrandchildA
|
^ (Preferences flow UP)
.preference(key:value:)
reduce function merges values from sibling subtrees.| Approach | Pros | Cons |
|---|---|---|
GeometryReader | Simple, immediate | Consumes all proposed space; disrupts layout |
Preference + background GeometryReader | Non-intrusive; child drives own size | More boilerplate |
Prefer the preference approach when measuring without affecting layout.
// ✅ Good: inject once, available everywhere
ContentView()
.environment(\.theme, appTheme)
.environment(\.networkService, liveNetwork)
// ❌ Bad: drilling a parameter through every init
ContentView(theme: appTheme)
// -> SubView(theme: appTheme)
// -> DetailView(theme: appTheme)
// -> CardView(theme: appTheme)
// ✅ Good: default value means views work without explicit injection
struct AccentStyleKey: EnvironmentKey {
static let defaultValue: Color = .accentColor
}
// ❌ Bad: force-unwrapping or crashing when no value is injected
struct AccentStyleKey: EnvironmentKey {
static let defaultValue: Color? = nil // then force-unwrapping later
}
// ✅ Good: pure function, predictable behavior
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
// ❌ Bad: side effects in reduce
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
let next = nextValue()
print("Reducing: \(value) with \(next)") // side effect
value = next
}
// ✅ Good: concise, less boilerplate (iOS 18+)
extension EnvironmentValues {
@Entry var accentStyle: Color = .blue
}
// ❌ Unnecessary when targeting iOS 18+: verbose three-part definition
struct AccentStyleKey: EnvironmentKey {
static let defaultValue: Color = .blue
}
extension EnvironmentValues {
var accentStyle: Color {
get { self[AccentStyleKey.self] }
set { self[AccentStyleKey.self] = newValue }
}
}
// ✅ Good: @Environment is populated when body is called
@Environment(\.theme) private var theme
var body: some View { RoundedRectangle(cornerRadius: theme.cornerRadius) }
// ❌ Bad: @Environment values are NOT available during init
init() { cornerRadius = 12 /* cannot read theme here */ }