---
title: 'Property Wrappers'
source: 'https://academia.sh/en/courses/swift/property-wrappers'
course: 'The Swift Language'
language: en
updated: '2026-10-21T05:03:12+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Writes a property wrapper with wrappedValue and projectedValue and reaches the projected value with the $ prefix'
    - 'Reads from the measurement output that the wrappedValue getter runs on every access'
    - 'Measures that the initial value given to a wrapper is evaluated once while the property is set up'
    - 'Shows that in a cached wrapper the evaluation moment is written in the body of the wrapper'
---

# Property Wrappers

Measures that the wrappedValue getter of a property wrapper runs on every access, that the initial value is set up once, and that a cached wrapper turns the moment into first use.

In the previous lesson a closure was a box that took a value into itself and stored it. A **property wrapper (`@propertyWrapper`)** builds a similar box for the property itself: how the value of the property is stored, and what happens when it is read or written, moves into a separate type, and the property is marked with the name of that type. The only required member of a wrapper type is `wrappedValue`; this member runs when the property is read and when it is written. The optional `projectedValue` member offers a second value that is reached by putting `$` in front of the property name.

This lesson writes a wrapper and measures three different moments of it: the moment the getter runs, the moment the initial value is evaluated, and the moment that changes when a cache is added. Most of the measurements use a `Clamped` wrapper, a small type that squeezes a value into a range.

```swift
// Probe.swift — records when an expression is evaluated
final class Probe {
    let label: String
    private(set) var evaluations = 0
    private(set) var uses = 0
    private var evaluatedBeforeUse = false

    init(_ label: String) {
        self.label = label
    }

    func eval<T>(_ value: T) -> T {
        evaluations += 1
        if uses == 0 { evaluatedBeforeUse = true }
        return value
    }

    func use<T>(_ access: () -> T) -> T {
        uses += 1
        return access()
    }

    var moment: String {
        if evaluations == 0 { return "never" }
        if evaluations > 1 { return "on-every-use" }
        return evaluatedBeforeUse ? "immediately" : "on-first-use"
    }

    func report() {
        print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
    }
}

func tally(_ probes: [Probe]) {
    for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
        let count = probes.filter { $0.moment == moment }.count
        print("\(moment): \(count)")
    }
}
```

```swift
// Clamped.swift — squeezes a value into a range
@propertyWrapper
struct Clamped {
    private var current: Int
    let range: ClosedRange<Int>
    let owner: Probe

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

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

    var projectedValue: Bool { current == range.upperBound }
}
```

## Writing the Wrapper

The `Clamped` wrapper holds an `Int` and squeezes the assigned value into the given range. The body of the `wrappedValue` getter calls `owner.eval` before it returns the value; every run of the getter is recorded this way. `projectedValue` is a `Bool` that says whether the value is at the upper bound, and it does not call `eval` when it is read, because what is measured is `wrappedValue`.

## The Getter Runs on Every Access

When the wrapper is used, the property is written as `@Clamped(0...10, owner: reads) var level = 5`: the values in parentheses go to the `init` method of the wrapper, and `= 5` goes to the `wrappedValue` parameter. The first measurement reads `level` twice and assigns a value outside the range in between. In the last line, `$level` reads the projected value.

```swift
// main.swift — the wrappedValue getter runs on every access
let reads = Probe("wrapper-read")

do {
    @Clamped(0...10, owner: reads) var level = 5
    print("level: \(reads.use { level })")
    level = 42
    print("level: \(reads.use { level })")
    print("at upper bound: \($level)")
}

reads.report()
```

```
level: 5
level: 10
at upper bound: true
wrapper-read: 2 evaluations, 2 uses -> on-every-use
```

The first read gives 5 and the second gives 10: the assigned 42 was squeezed to the upper bound of the range. `$level` reads `projectedValue`, and because the value is at the upper bound it writes `true`. `wrapper-read` records two uses and two evaluations; because the getter runs on every access, its moment is `on-every-use`. The `$level` read did not raise the record, because it calls a different member. This lesson's moment is **on every use**.

## The Initial Value Is Set Up Once

The `= 5` written on the property is evaluated not on every run of the getter but once, while the property is set up. The second measurement wraps the initial value in `start.eval(5)` and reads the property twice. The getter's own `eval` call writes to a different `Probe`, `quiet`, so `start` keeps the record of the initial value only.

```swift
// main.swift — the initial value is evaluated once while the property is set up
let start = Probe("initial-value")
let quiet = Probe("quiet-read")

do {
    @Clamped(0...10, owner: quiet) var level = start.eval(5)
    print("level: \(start.use { level }) \(start.use { level })")
}

start.report()
```

```
level: 5 5
initial-value: 1 evaluations, 2 uses -> immediately
```

Both reads give 5, and `initial-value` records a single evaluation. Because this evaluation happened before the first `use` call, its moment is `immediately`. The getter ran twice, but it did not evaluate the initial expression again: the `init` method of the wrapper took the value of the expression once and wrote it to `current`.

```swift
// Cached.swift — computes the value on first read and stores it
@propertyWrapper
final class Cached {
    private var memo: Int?
    private let make: () -> Int

    init(wrappedValue make: @autoclosure @escaping () -> Int) {
        self.make = make
    }

    var wrappedValue: Int {
        if let memo { return memo }
        let result = make()
        memo = result
        return result
    }
}
```

## The Setter Runs on Every Assignment Too

`wrappedValue` works in both directions: `get` runs on a read, and `set` runs on an assignment. The initial value, on the other hand, does not pass through `set`; the `init` method stores it directly. This difference can be measured separately on the write side. The `Counted` wrapper calls `owner.eval` in the `set` body and applies no limit. The measurement starts the property at 0 and then increases it three times.

```swift
// Counted.swift — records every assignment
@propertyWrapper
struct Counted {
    private var current: Int
    let owner: Probe

    init(wrappedValue: Int, owner: Probe) {
        self.current = wrappedValue
        self.owner = owner
    }

    var wrappedValue: Int {
        get { current }
        set { current = owner.eval(newValue) }
    }
}
```

```swift
// main.swift — set runs on every assignment, the initial value skips set
let writes = Probe("wrapper-write")

do {
    @Counted(owner: writes) var score = 0
    for amount in 1...3 {
        _ = writes.use { score += amount }
    }
    print("score: \(score)")
}

writes.report()
```

```
score: 6
wrapper-write: 3 evaluations, 3 uses -> on-every-use
```

The score is 6, because 1, 2 and 3 were added. `wrapper-write` records three uses and three evaluations: the initial `= 0` assignment did not enter the evaluation count, only the three increases did. The read and write sides follow the same rule: a member of the wrapper runs on every access, while the initial value is taken once in the `init` method.

## The Projected Value

`Clamped` offers two things: the property itself through `wrappedValue`, and an extra piece of information carried beside it through `projectedValue`. The extra information is reached by putting `$` in front of the property name. This `$` read does not call the `wrappedValue` getter; it is a separate member. The measurement builds two identical wrappers with separate `Probe` objects and assigns both a value outside the range. It reads one through `$` and the other directly.

```swift
// main.swift — a $ read does not call the wrappedValue getter
let projected = Probe("projected-value")
let plain = Probe("direct-read")
do {
    @Clamped(0...10, owner: projected) var left = 5
    @Clamped(0...10, owner: plain) var right = 5
    left = 50
    right = 50
    print("at upper bound: \(projected.use { $left }) \(projected.use { $left })")
    print("value: \(plain.use { right }) \(plain.use { right })")
}
projected.report()
plain.report()
```

```
at upper bound: true true
value: 10 10
projected-value: 0 evaluations, 2 uses -> never
direct-read: 2 evaluations, 2 uses -> on-every-use
```

Both properties were assigned 50, and both were squeezed to 10. The `$left` read gives `true`: the value is at the upper bound. But this read does not go through the `wrappedValue` getter. `projected-value` records zero evaluations against two uses and says `never`, because there is no `eval` in the body of `projectedValue` and `current` is compared directly. `direct-read` records two evaluations in the same two uses and says `on-every-use`. A `$` read does not run the `wrappedValue` getter; it is a separate member: the `$` spelling asks the wrapper a question that is independent of the side effects of the `wrappedValue` getter. The moment question is therefore asked separately for each access: `left` and `right` are the same wrapper type, but which getter runs depends on how the access is written.

## Wrapper Arguments Are Evaluated Once

The expressions in parentheses in `@Clamped(0...10, owner: quiet)` go to the `init` method of the wrapper. The compiler turns the line `@Clamped(...) var level = 5`, roughly, into a call `Clamped(wrappedValue: 5, 0...10, owner: quiet)` that runs once while the property is set up. This measurement tests both the property form and a hand-written `init` call. The upper bound of the range and the initial value pass through `Probe`.

```swift
// main.swift — wrapper arguments are evaluated once while the property is set up
let ceiling = Probe("range-argument")
let explicit = Probe("explicit-init")
let quiet = Probe("quiet")
do {
    @Clamped(0...ceiling.eval(10), owner: quiet) var level = 5
    print("level: \(ceiling.use { level }) \(ceiling.use { level })")

    let wrapper = Clamped(wrappedValue: explicit.eval(5), 0...10, owner: quiet)
    print("explicit: \(explicit.use { wrapper.wrappedValue }) \(explicit.use { wrapper.wrappedValue })")
}
ceiling.report()
explicit.report()
```

```
level: 5 5
explicit: 5 5
range-argument: 1 evaluations, 2 uses -> immediately
explicit-init: 1 evaluations, 2 uses -> immediately
```

On both paths the counter says the same thing. `range-argument` records a single evaluation, and because that evaluation happened before the first `use` call, its moment is `immediately`. The two reads did not recompute the upper bound of the range: `ceiling.eval(10)` ran once while the property was set up, and the `ClosedRange` value stayed inside the wrapper. The hand-written call `Clamped(wrappedValue: explicit.eval(5), ...)` gives the same result: `explicit-init` records a single evaluation and says `immediately`. The wrapper syntax introduces no new evaluation rule; it is a short way of writing an `init` call, and the arguments of the call are evaluated once at the moment of the call, as always. How many times the getter runs and how many times the argument is evaluated are two separate numbers: the first rises with every access, and the second is fixed once while the property is set up. This distinction requires choosing, from the start, which number is being looked at when a wrapper is read.

## Composing Wrappers

More than one wrapper can be written on a property. The one on the left is outer, and the one on the right is inner. The `Traced` below is a generic wrapper; it stores the value as it is and calls `owner.eval` on every read. The measurement puts it outside `Clamped`. It reads the property twice and looks at how many times the getter of each layer runs.

```swift
// Traced.swift — a generic wrapper that records every read
@propertyWrapper
struct Traced<Value> {
    private var current: Value
    let owner: Probe

    init(wrappedValue: Value, owner: Probe) {
        self.current = wrappedValue
        self.owner = owner
    }

    var wrappedValue: Value {
        get { owner.eval(current) }
        set { current = newValue }
    }
}
```

```swift
// main.swift — two wrappers stacked
let outer = Probe("outer-wrapper")
let inner = Probe("inner-wrapper")
do {
    @Traced(owner: outer) @Clamped(0...10, owner: inner) var level = 50
    print("read: \(outer.use { inner.use { level } }) \(outer.use { inner.use { level } })")
    level = 3
    print("after write: \(outer.use { inner.use { level } })")
}
outer.report()
inner.report()
```

```
read: 10 10
after write: 3
outer-wrapper: 4 evaluations, 3 uses -> on-every-use
inner-wrapper: 3 evaluations, 3 uses -> on-every-use
```

The first two reads give 10: the initial value was 50, and the innermost `Clamped` had squeezed it to 10 inside `init`. After the assignment `level = 3`, the read gives 3. The counters are worth noting: the getter of the inner wrapper runs three times in three reads. The getter of the outer wrapper runs four times: three reads and the assignment itself. The assignment calls the outer getter one more time, because the outer wrapper first reads its value and then changes it on the inner `Clamped`. Composition therefore counts the number of runs of each of the two layers separately. Both `Probe` objects say `on-every-use`. Adding a layer adds a chain of `get` and `set` to every read and write. When a wrapper whose getter has side effects is placed in a composition, this extra work has to be taken into account. In this measurement the outer getter ran four times and the inner getter three times; compositions with more layers were not measured.

## The Bounding Measurement

That the initial value is `immediately` is a result of how `Clamped` is written, not of the wrapper syntax. The `Cached` wrapper below turns the expression given to the `init` method into a closure with `@autoclosure` and does not call it until `wrappedValue` is first read. The result is stored in a property; later reads return the stored value. The same `@` sign and the same `= expression` spelling, but the evaluation moment has changed.

```swift
// main.swift — a cached wrapper turns the moment into first use
let lazyValue = Probe("cached-wrapper")

do {
    @Cached var total = lazyValue.eval(7)
    print("total: \(lazyValue.use { total }) \(lazyValue.use { total })")
}

lazyValue.report()
```

```
total: 7 7
cached-wrapper: 1 evaluations, 2 uses -> on-first-use
```

Both reads give 7. `cached-wrapper` records a single evaluation, but this time the evaluation happened inside the first `use` call, so its moment is `on-first-use`. The `@Cached` spelling looked like the `@Clamped` spelling; what makes the difference is whether the expression is used at once in the `init` method of the wrapper or held back in a closure. The moment is written in the body of the wrapper, not in the `@` syntax.

**MA4.** Both measured wrappers hold an `Int` value. That generic wrappers and wrappers used as class properties follow the same rules was not measured in this lesson. Use as a local variable is a design choice.

## Summary

- A property wrapper is marked with `@propertyWrapper`; `wrappedValue` runs when the property is read and written, and `projectedValue` is read with the `$` prefix.
- The `wrappedValue` getter runs on every access; an assignment is carried out by the rule of the wrapper, for example by squeezing into a range.
- The initial value written on the property is evaluated once while the property is set up, in a wrapper such as `Clamped`.
- `Cached`, written with `@autoclosure` and a stored result, turns the same spelling into a single evaluation on first use; the moment is in the body of the wrapper.
- This lesson's moment: **on every use**. The `wrappedValue` getter runs on every access.

## Next Step

In this lesson a wrapper moved the value rule of a property into a separate type. Result builders apply the same idea to a list of expressions: instead of being evaluated one by one, the lines in a block are given to methods of a builder type such as `buildBlock`, and that type decides which line runs at which moment. The next lesson writes a result builder of its own and measures that the expressions of the branch that is not chosen are never evaluated.
