What does the compiler turn a property wrapper into: what storage exists, what are $name and _name, and what does it cost?
Predict first
What does this print?
@propertyWrapper struct Clamped {
private var v: Int
var wrappedValue: Int { get { v } set { v = min(newValue, 10) } }
var projectedValue: Bool { v == 10 }
init(wrappedValue: Int) { v = min(wrappedValue, 10) }
}
struct S { @Clamped var n = 5 }
var s = S()
s.n = 99
print(s.n, s.$n, MemoryLayout<S>.size, Mirror(reflecting: s).children.map { $0.label! })
- 10 true 8 ["_n"]
- 99 false 8 ["n"]
- 10 true 16 ["n", "$n"]
- 10 true 1 ["_n"]
Predict first
What synthesised declarations does the AST contain?
import Swift
@propertyWrapper struct Clamped {
private var v: Int
var wrappedValue: Int { get { v } set { v = min(newValue, 10) } }
var projectedValue: Bool { v == 10 }
init(wrappedValue: Int) { v = min(wrappedValue, 10) }
}
struct S { @Clamped var n = 5 }
- an implicit _n of type Clamped (private) and $n of type Bool
- only n
- a class named S_n
- nothing extra
Mental model. A property wrapper is a mailroom clerk standing in front of your mailbox: you say “put this in n” and the clerk (the wrapper) applies the rules and files it in a box named _n that you never see.
A property wrapper is a rewrite. For @Clamped var n = 5 the compiler synthesises a private stored property _n of the wrapper type, and turns n into a computed property whose getter and setter call _n.wrappedValue. If the wrapper has a projectedValue, it also adds $n, which returns it. The initial value 5 is passed to init(wrappedValue:) when the instance is created. The AST dump shows exactly this: an implicit _n of type Clamped and an implicit $n of type Bool. The run confirms the consequences: the struct’s size is that of the wrapper’s storage (8 bytes), and Mirror reports the child as _n, not n.
Costs and traps follow. A wrapper with simple stored state is optimised away in -O like hand-written accessors, but one that holds a class, a lock or an allocation pays that on every instance. Because the real storage is _n, reflection, synthesised Codable (which uses the wrapper’s own conformance), memberwise initialisers (which take the wrapped type unless you opt in) and KVC-style tools see the wrapper, not the name you wrote. Wrappers cannot be applied to a computed property or used where the type has to be inferred from several declarations, and in Swift 6 the wrapper’s isolation and Sendable-ness is checked on the synthesised storage. @State, @Published and @AppStorage are ordinary wrappers with extra runtime hooks.
n clamps to 10, $n is the projected value, storage is one Int named _n.
The compiler adds backing storage and the projection.
Common traps
- Expecting Mirror or logging to show the property name. You see
_n. - Putting a heavy reference type in a wrapper used by many value types. Each instance pays for it.
They might follow up with
- How does SwiftUI know when
@Statechanges? The wrapper’s storage lives outside the view value and signals the update; the wrapper pattern is only the entry point. - Can wrappers compose? Yes, each layer wraps the next; order matters.
Related questions
- 1.6.3 What is a property wrapper? Can you write a custom one?
- 1.6.5 Are property wrappers thread-safe? Is lazy?
- 1.10.3 Macros vs property wrappers vs result builders — one sentence each.
- 5.8.4 What reflection metadata does the compiler emit for every type, what depends on it, and what does…
- 3.4.2 What do MemoryLayout’s size, stride and alignment mean, and why does field order matter?
What does the compiler do with the statements inside a result builder… →