The Underground Wrapper Scene
10 SwiftUI Property Wrappers and Values You Probably Don’t Know… But Should
There I was, staring at my beautiful new experience written entirely in SwiftUI, months ahead of schedule as I promised (and gambled on). Feeling proud of myself, I went to show one of my teammates. They praised the work… and then asked how it looked in different dynamic sizes.
Uh oh…
Much of the UI began to shrink and grow as we scaled from small to large. But the sub-containers that made up such a large part of the experience were set to a fixed height and width.
I quickly scrambled to find a solution, worrying that it would take another sprint to custom write something. But then, what’s this? @ScaledMetric
you say? I quickly add it to my code.
That sense of aw and magic I had when I first used SwiftUI quickly washed over me as I watched these containers scale along with the overall experience. The fear of a lost sprint was squashed with a simple, mysterious property wrapper.
Wrappers: Unsung Heroes of SwiftUI
SwiftUI developers lean heavily on the classic property wrappers: @State
, @Binding
, @ObservedObject
. But SwiftUI ships with a powerful cast of other property wrappers and wrapped values — some that solve complex problems with a single line of code.
This article is your curated guide to 10 wrappers and values that are often overlooked but deserve a spot in your toolkit.
Note: What do I mean by wrapped values? Towards the end of this list, you’ll see values that can be found in Environment. They are not wrappers, themselves. However, they felt at home on this list because of the similar help they offer experiences as well as to highlight some of the powerful values being kept “under wraps”.
What Are Property Wrappers?
Property wrappers in Swift are a powerful language feature that let you define reusable behaviors for how values are stored, computed, or observed. In SwiftUI, they’re everywhere: @State
, @Binding
, @Environment
, and others all rely on this system.
At a high level, a property wrapper:
Encapsulates a value (like a
Bool
orString
)Adds logic to how that value is read or written
Can expose additional projected values (like
$binding
)
For example, @State var isOn = false
actually wraps the value inside a State<Bool>
struct, managing updates and triggering view re-renders when the value changes.
Want to Make Your Own?
If you’re curious about how custom wrappers are built, Apple has a concise guide here: Swift Documentation – Property Wrappers
Come join The Captain and the rest of the Captain’s Crew at Office Hours!
Open to all paid subscribers! Join at the button below and take advantage of a special promo discount!
The Main Event
And now it’s time to reveal some of the best Wrappers in the SwiftUI Game…
1. @ScaledMetric
– Auto-Scale UI for Dynamic Type
What it does:
Automatically scales a numeric value (padding, size, etc.) based on the user's preferred text size.
@ScaledMetric var size: CGFloat = 64
Image("avatar")
.resizable()
.frame(width: size, height: size)
Why it matters:
Adapts automatically to accessibility settings without manual recalculation.
Great for spacing, frames, and icons that need to feel "right" across small and large text settings.
Helps you avoid hardcoding fixed dimensions that break UX for accessibility users.
When to use it:
When sizing UI elements that should grow naturally with Dynamic Type — like buttons, avatars, cards, or tap targets.
📚 Learn more about @ScaledMetric
2. @Namespace
– Enable Matched Geometry Transitions
What it does:
Creates a transition context to morph views between states.
@Namespace var animation
matchedGeometryEffect(id: "avatar", in: animation)
Why it matters:
Enables beautiful hero transitions where views fluidly animate across different screens.
Helps maintain spatial continuity for users (which improves UX dramatically).
Way less boilerplate than doing custom animations manually.
When to use it:
Any time you're animating between screens or states — like photo galleries, card swipes, onboarding flows, or navigation transitions.
3. @FocusedValue
– Share Values Across Focused Views
What it does:
Exposes values based on the currently focused view. Perfect for input coordination.
@FocusedValue(\.username) var username
Why it matters:
Great for building forms where data flows between fields without tightly coupling the views.
Lets you create smarter, context-aware toolbars or keyboards that change depending on focus.
When to use it:
When multiple fields or child views need to share or react to information based on the current focus — like dynamic toolbars or editing panels.
📚 Learn more about @FocusedValue
4. @GestureState
– Track Gesture Values
What it does:
Tracks temporary state during a gesture and resets automatically after it ends.
@GestureState private var isPressed = false
Circle()
.scaleEffect(isPressed ? 1.2 : 1.0)
.gesture(
LongPressGesture()
.updating($isPressed) { value, state, _ in state = value }
)
Why it matters:
No need to manually reset gesture state — SwiftUI cleans it up for you.
Makes it easy to animate elements during gestures like drags, long-presses, or swipes.
When to use it:
Anytime you want temporary visual feedback during a user interaction — like highlighting, scaling, or revealing hidden options while dragging.
📚 Learn more about @GestureState
5. @FocusState
– Control Input Focus Declaratively
What it does:
Manage keyboard focus between multiple fields using simple state binding.
@FocusState private var focusedField: Field?
TextField("Username", text: $username)
.focused($focusedField, equals: .username)
Why it matters:
Cleaner and safer than UIKit’s messy
becomeFirstResponder()
/resignFirstResponder()
.Crucial for building great, intuitive SwiftUI forms, especially for keyboard users.
When to use it:
Whenever you want to control or track which field is active — like auto-advancing fields, validating on focus loss, or dismissing keyboards cleanly.
📚 Learn more about @FocusState
6. @AppStorage
– Persist User Defaults with Zero Boilerplate
What it does:
Binds directly to UserDefaults
with reactive updates.
@AppStorage("isDarkMode") var isDarkMode = false
Why it matters:
You get persistence and view updates automatically.
No more manually syncing between UI state and stored settings.
When to use it:
For lightweight user preferences — like theme modes, onboarding completion, toggle switches, or any user-specific flags.
📚 Learn more about @AppStorage
7. @SceneStorage
– Persist View State Across Sessions
What it does:
Restores view state like scroll position, tab selection, or input text.
@SceneStorage("draftText") var text: String = ""
Why it matters:
Keeps your app feeling seamless even when a scene or window is closed and reopened.
Essential for iPad multitasking and Stage Manager where apps frequently suspend and resume.
When to use it:
For view-local state that should survive temporary app interruptions — like scroll positions, selected tabs, or form drafts.
📚 Learn more about @SceneStorage
8. @Environment(\.dynamicTypeSize)
– Read Current Type Size
What it does:
Gives you access to the current Dynamic Type size setting (like .large
, .accessibility5
).
@Environment(\.dynamicTypeSize) var typeSize
if typeSize.isAccessibilitySize {
CompactLayout()
} else {
RegularLayout()
}
Why it matters:
Lets you adapt layouts, not just text size, for accessibility.
You can build UIs that totally rethink structure at large text sizes.
When to use it:
When you need major layout shifts for users with accessibility text sizes — like stacking content vertically or increasing tap target spacing.
📚 Learn more about EnvironmentValues/dynamicTypeSize
9. @Environment(\.horizontalSizeClass)
– Build Responsive Layouts
What it does:
Lets you respond to size class changes (compact vs regular).
@Environment(\.horizontalSizeClass) var sizeClass
if sizeClass == .compact {
VerticalLayout()
} else {
HorizontalLayout()
}
Why it matters:
Helps you create adaptive interfaces that work on iPhone, iPad, and Mac seamlessly.
Vital for Split View, Stage Manager, and dynamic resizing scenarios.
When to use it:
Whenever you want a view to layout differently based on device, orientation, or window size.
📚 Learn more about EnvironmentValues/horizontalSizeClass
10. @Environment(\.isEnabled)
– Respect System Interactivity
What it does:
Check whether your view or container is enabled or disabled.
@Environment(\.isEnabled) var isEnabled
Text("Label")
.opacity(isEnabled ? 1 : 0.5)
Why it matters:
Lets you customize how views behave when
.disabled()
is applied upstream.You can build better, more consistent disabled states beyond just graying out a button.
When to use it:
When creating custom components that need to respect (or override) enabled/disabled state from parent views.
📚 Learn more about EnvironmentValues/isEnabled
Wrap Up: These Wrappers Pull Serious Weight
If you’re building modern, accessible, adaptive SwiftUI interfaces — don’t stop at @State. The wrappers above help you:
Write cleaner code
Support accessibility out of the box
Handle persistence, focus, gestures, layout, and more