Skip to main content

Property Wrappers & Projected Values

A plain-English dictionary for Swift property wrappers — what @propertyWrapper, wrappedValue, and projectedValue actually do, why they exist, and how to write your own without getting confused by the dollar sign.

Property wrappers are one of those Swift features that feel magic until you look at what the compiler is actually generating. Once you see that @State var count = 0 is just syntactic sugar for a hidden struct with a get/set, everything clicks — including why $count gives you something completely different.


@propertyWrapper

What it is: a Swift attribute you put on a struct, class, or enum to tell the compiler “this type can be used as a wrapper around another value.” Once you mark a type @propertyWrapper, you can use it as an attribute on stored properties with @YourWrapper var name: ValueType.

Why it exists: to stop you writing the same boilerplate validation, storage, or observation logic in every type that needs it. Before property wrappers, clamping a value between 0 and 100 meant writing the same computed property with didSet or a private backing variable in every single struct that cared. With a wrapper, you write it once.

How to declare one:

@propertyWrapper
struct Clamped {
    private var value: Int
    let range: ClosedRange<Int>

    var wrappedValue: Int {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }

    init(wrappedValue: Int, _ range: ClosedRange<Int>) {
        self.range = range
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }
}

struct Thermostat {
    @Clamped(0...100) var temperature: Int = 20
}

var t = Thermostat()
t.temperature = 150   // silently clamped
print(t.temperature)  // 100

The compiler rewrites @Clamped(0...100) var temperature: Int = 20 into a hidden stored property of type Clamped and exposes temperature as a computed property that forwards to its wrappedValue.


wrappedValue

What it is: the required property every @propertyWrapper type must declare. It defines the type and behavior of the property as seen from the outside — the thing you get when you write myObject.propertyName.

Why it matters: the name is not arbitrary. The compiler looks specifically for wrappedValue when generating the forwarding accessors. If you name it anything else, the build fails.

How the compiler uses it:

When you write:

struct Player {
    @Clamped(0...100) var health: Int = 100
}

The compiler generates roughly this behind the scenes:

struct Player {
    private var _health: Clamped = Clamped(wrappedValue: 100, 0...100)

    var health: Int {
        get { _health.wrappedValue }
        set { _health.wrappedValue = newValue }
    }
}

You never write _health yourself — it’s a compiler-generated private stored property. All reads and writes to health go through the wrapper’s wrappedValue accessors.

The init rule: if your wrapper has an init(wrappedValue:), the caller can provide an initial value with = someValue syntax. If it has init(wrappedValue:someParam:), the attribute syntax provides someParam and = someValue provides wrappedValue. If the wrapper has no init(wrappedValue:) at all, the property cannot use = value syntax and must be initialized through the wrapper directly.


projectedValue

What it is: an optional second property you can add to a @propertyWrapper type. When it exists, writing $propertyName in code gives you whatever projectedValue returns, which can be a completely different type from wrappedValue.

Why it exists: sometimes the wrapper needs to expose more than just the stored value. SwiftUI’s @State stores an Int as wrappedValue, but through $count it exposes a Binding<Int> — a two-way connection to the source of truth that can be passed down to child views. Without projectedValue, you’d need a separate API surface to get that binding.

How to add one:

@propertyWrapper
struct Logged<Value> {
    private var storedValue: Value
    private(set) var history: [Value] = []

    var wrappedValue: Value {
        get { storedValue }
        set {
            history.append(storedValue)
            storedValue = newValue
        }
    }

    // projectedValue exposes the full wrapper itself
    var projectedValue: Logged<Value> { self }

    init(wrappedValue: Value) {
        self.storedValue = wrappedValue
    }
}

struct Account {
    @Logged var balance: Double = 0.0
}

var account = Account()
account.balance = 500.0
account.balance = 750.0

print(account.balance)           // 750.0  — wrappedValue, via normal property name
print(account.$balance.history)  // [0.0, 500.0]  — projectedValue via $ prefix

projectedValue can return anything — the wrapper itself, a Binding, a Publisher, a read-only snapshot. The type is entirely up to you.


The $ Prefix

What it is: syntactic sugar for accessing projectedValue. Writing $name on a property wrapped with @SomeWrapper compiles to _name.projectedValue.

Why the dollar sign: it was borrowed from Combine’s @Published convention where $value gives you the Publisher. The symbol has no special meaning in the language itself — it is purely a naming convention made concrete by the compiler.

How to read it in context:

SyntaxAccessesType (for @State var count: Int)
countwrappedValueInt
$countprojectedValueBinding<Int>
_countthe wrapper itselfState<Int>

The underscore prefix gives you the raw wrapper instance — useful when you need to call a method on the wrapper directly, or when you’re initializing it manually.

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("\(count)")          // reads wrappedValue: Int
            Stepper("Count", value: $count)  // passes projectedValue: Binding<Int>
        }
    }
}

SwiftUI Wrappers Demystified

The SwiftUI property wrappers all follow the same pattern — once you know wrappedValue vs projectedValue, their behavior is predictable.

WrapperwrappedValue typeprojectedValue typeTypical use
@StateTBinding<T>Local mutable state owned by this view
@BindingTBinding<T>Reference to state owned elsewhere
@PublishedTPublisher<T, Never>Observable value inside an ObservableObject
@ObservedObjectT: ObservableObjectObservedObject<T>.WrapperExternal model object passed in
@EnvironmentObjectT: ObservableObjectObservedObject<T>.WrapperModel injected through the environment
@EnvironmentTnoneRead-only environment value
@AppStorageTBinding<T>Persisted value backed by UserDefaults

The rule of thumb: if you need to pass the value down to a child view that can mutate it, pass $propertyName. If you only need the current value for display, pass propertyName.


Writing a Wrapper with a Projected Binding

A common pattern is to expose a Binding as the projectedValue so your custom wrapper integrates with SwiftUI the same way @State does.

@propertyWrapper
struct Validated<Value> {
    private var value: Value
    let validate: (Value) -> Bool

    var wrappedValue: Value {
        get { value }
        set { value = newValue }
    }

    var isValid: Bool { validate(value) }

    // Expose a Binding so SwiftUI controls can write back to us
    var projectedValue: Binding<Value> {
        Binding(
            get: { self.value },
            set: { self.value = $0 }
        )
    }

    init(wrappedValue: Value, validate: @escaping (Value) -> Bool) {
        self.value = wrappedValue
        self.validate = validate
    }
}

struct SignupForm: View {
    @Validated(validate: { !$0.isEmpty }) var username: String = ""

    var body: some View {
        VStack {
            TextField("Username", text: $username)  // $username: Binding<String>
            if !_username.isValid {                 // _username: Validated<String>
                Text("Required").foregroundStyle(.red)
            }
        }
    }
}

Common Pitfalls

Using self inside projectedValue on a struct: if your wrapper is a struct and projectedValue captures self in a closure (as in the Binding pattern above), Swift requires the property to be mutating or the wrapper to be a class. The safer pattern is to make the Binding capture the value by copy, or make the wrapper a class if mutation through the binding needs to propagate back.

Forgetting init(wrappedValue:) for default value syntax: if you want @MyWrapper var x = someDefault, the wrapper must have an init(wrappedValue:). Without it, the compiler rejects the = someDefault and requires you to initialize through the attribute: @MyWrapper(someParam:) var x.

Accessing the wrapper type in a protocol: protocols cannot directly require a property to have a specific wrapper. Instead, declare the requirement in terms of wrappedValue type, and let the conforming type choose the wrapper.

Mutation from a non-mutating context: because the compiler stores the wrapper as a private var _name, using a property wrapper on a stored property in a struct always implies the struct must be var to mutate. A let instance of a struct freezes the wrapper and prevents writes to wrappedValue.