← All daysDay 1 of 49 · Part 1: Modern Swift: what changed since 2016
Day 1

Build your own property wrappers

Lesson 1: Property wrappers

You will build@Clamped, a small version of @AppStorage called @Stored, and a small version of Binding called Ref.

TimeAbout 80 minutes

WhySwiftUI is full of things like @State, @Binding and $text. They look like special syntax. They are not: they are an ordinary Swift feature called property wrappers. Today you write three of your own. After that, the SwiftUI ones will not be mysterious, because you will have built the same thing.

Step 1

Set up the project in Xcode

Today's project is a command-line program: no window and no simulator. You type code, run it, and read what it prints in Xcode's console. It is the quickest way to try out a language feature. Set it up from scratch:

  1. Open Xcode and choose File ▸ New ▸ Project… (⇧⌘N). If you are looking at the Welcome to Xcode window instead, its New Project menu offers the same choices.

  2. Xcode 27 shows a short list of project kinds. Click Choose Template.

  3. A sheet titled Choose a template for your new project appears. In the row of platforms at the top, click macOS. Under Application, select Command Line Tool. Click Next.

  4. Fill in the options. Product Name: Wrappers. Team: leave it as it is — a command-line program does not need one. Organization Identifier: the one you normally use, such as com.yourname. Language: Swift. Click Next.

  5. Choose the folder to keep the project in — your Development folder, for example. Tick Create Git repository on my Mac or not, as you prefer. Click Create.

  6. The project window opens. In the Project navigator on the left, open the Wrappers folder and click main.swift. Below a comment that gives the file's name and today's date, Xcode has written:

    main.swift, as Xcode made it
    import Foundation
    
    print("Hello, World!")
  7. Check that the run destination in the toolbar says My Mac, then choose Product ▸ Run (⌘R). The debug area opens at the bottom of the window, and its console shows:

    Console
    Hello, World!
    Program ended with exit code: 0

    The last line comes from Xcode, not from the program: it says the program has finished, and 0 means it finished without an error. If you cannot see the console, choose View ▸ Debug Area ▸ Activate Console (⇧⌘C).

  8. Now add a second file. With main.swift still selected, choose File ▸ New ▸ File from Template… (⌘N). Under Source, select Swift File. Click Next.

  9. Name it Wrappers, leave the other settings as they are, and click Create. The file appears beside main.swift and opens with a comment and one line of code: import Foundation. Leave that line where it is.

From here on, every step works the same way. The types you build go in Wrappers.swift. The code that tries them out goes in main.swift, below import Foundation. A file with that exact name is special: the code in it runs from top to bottom when the program starts.

A faster way, for when you need it

The list in step 2 also offers Command Line Tool itself, described as "Terminal program for macOS". One click on it opens an untitled project straight away, with the same two lines in main.swift and no questions asked. That is the one to use when you only want to try a few lines — in an interview, for example. These tutorials use Choose Template so that every project has a name and a folder from the start.

Step 2

The problem: a value that must stay in range

Imagine a volume control whose level must stay between 0 and 10. Without any new features, you would hide the stored value and check it in a setter:

Add to Wrappers.swift
struct PlainVolume {
    private var storedLevel = 5

    var level: Int {
        get { storedLevel }
        set { storedLevel = min(max(newValue, 0), 10) }     // keep it within 0...10
    }
}
In main.swift, replace everything below import Foundation with
var volume = PlainVolume()
volume.level = 42
print(volume.level)

Run it (⌘R). The console shows

Console
10
Program ended with exit code: 0

It works. But every property that needs clamping needs the same six lines, with only the names changed. Add a bass and a treble and you have written it three times. A property wrapper lets you write it once.

Step 3

Move the logic into a wrapper

A property wrapper is a type marked @propertyWrapper that has a property named wrappedValue. Put the clamping logic there:

Add to Wrappers.swift
@propertyWrapper
struct ClampedToTen {
    private var value = 0

    var wrappedValue: Int {
        get { value }
        set { value = min(max(newValue, 0), 10) }
    }
}

Now use it. Writing @ClampedToTen in front of a property attaches the wrapper to it:

Add to Wrappers.swift
struct SimpleVolume {
    @ClampedToTen var level: Int
    @ClampedToTen var bass: Int
}
In main.swift, replace everything below import Foundation with
var volume = SimpleVolume()
volume.level = 42
volume.bass = -7
print(volume.level, volume.bass)

Run it (⌘R). The console shows

Console
10 0
Program ended with exit code: 0

Two clamped properties, and the clamping logic exists in one place.

What the compiler did

There is no magic here. When you write @ClampedToTen var level: Int, the compiler stores a ClampedToTen value in a hidden property named _level, and turns level into a computed property that reads and writes _level.wrappedValue. You could have written that by hand:

For comparison only — you do not need to type this
struct HandWrittenVolume {
    private var _level = ClampedToTen()        // the hidden storage: the wrapper itself

    var level: Int {
        get { _level.wrappedValue }
        set { _level.wrappedValue = newValue }
    }
}

That hand-written version behaves identically. A property wrapper is a shorter way to write it.

Step 4

Give the wrapper an argument and a starting value

ClampedToTen only knows one range. A useful wrapper takes the range as an argument, and works for any type that can be compared. Add a generic version (leave ClampedToTen where it is, so the earlier code still compiles):

Add to Wrappers.swift
@propertyWrapper
struct Clamped<Value: Comparable> {
    private var value: Value
    private let range: ClosedRange<Value>

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

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

The initializer is the important part. When you attach a wrapper like this —

Add to Wrappers.swift
struct Mixer {
    @Clamped(0...10) var level = 5
    @Clamped(0.0...1.0) var balance = 0.5
}

— the compiler calls init(wrappedValue:_:). The value after = arrives as wrappedValue. The arguments in parentheses arrive after it. That is why the first parameter has to be named exactly wrappedValue.

In main.swift, replace everything below import Foundation with
var mixer = Mixer()
mixer.level = 42
mixer.balance = -3
print(mixer.level, mixer.balance)

Run it (⌘R). The console shows

Console
10 0.0
Program ended with exit code: 0

One wrapper now clamps an Int to 0…10 and a Double to 0…1.

Step 5

Add a projected value: the $ prefix

A wrapper can expose a second value besides the wrapped one. It is called the projected value, and you reach it by putting $ in front of the property's name. You decide what it is. Here, make it report whether the last assignment had to be clamped. Replace Clamped with this version — the changed lines are the projectedValue property and the setter:

In Wrappers.swift, replace Clamped with this
@propertyWrapper
struct Clamped<Value: Comparable> {
    private var value: Value
    private let range: ClosedRange<Value>
    private(set) var projectedValue = false      // was the last write out of range?

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

    var wrappedValue: Value {
        get { value }
        set {
            value = min(max(newValue, range.lowerBound), range.upperBound)
            projectedValue = (value != newValue)
        }
    }
}
In main.swift, replace everything below import Foundation with
var mixer = Mixer()
mixer.level = 7
print(mixer.level, mixer.$level)

mixer.level = 42
print(mixer.level, mixer.$level)

Run it (⌘R). The console shows

Console
7 false
10 true
Program ended with exit code: 0

mixer.level is the wrapped value. mixer.$level is the projected value. That is all $ means, anywhere in Swift.

Why this matters in SwiftUI

When you see TextField("Name", text: $name) in SwiftUI, the $ is doing exactly this. name is declared with @State, and the projected value of @State happens to be a Binding. You will build your own Binding in step 7.

Step 6

Build a small @AppStorage

Wrappers are not limited to checking values. They can decide where a value is stored. This one keeps its value in UserDefaults, so it survives between runs of the program:

Add to Wrappers.swift
@propertyWrapper
struct Stored<Value> {
    private let key: String
    private let defaultValue: Value

    init(wrappedValue: Value, _ key: String) {
        self.key = key
        self.defaultValue = wrappedValue
    }

    var wrappedValue: Value {
        get { UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue }
        nonmutating set { UserDefaults.standard.set(newValue, forKey: key) }
    }
}
Add to Wrappers.swift
struct Preferences {
    @Stored("username") var username = "guest"
}
In main.swift, replace everything below import Foundation with
UserDefaults.standard.removeObject(forKey: "username")     // start fresh each run

let preferences = Preferences()        // note: `let`, not `var`
print(preferences.username)

preferences.username = "andrew"        // allowed on a `let` — the setter is nonmutating
print(Preferences().username)          // a brand-new instance sees the saved value

Run it (⌘R). The console shows

Console
guest
andrew
Program ended with exit code: 0

Look closely at two things in that output. A brand-new Preferences() saw the value that the first one saved — because the value is not stored in the struct at all. And preferences was declared with let, yet you assigned to preferences.username.

What `nonmutating set` means

Normally, setting a property changes the struct, so the struct must be a var. The word nonmutating tells the compiler that this setter does not change the struct — it changes something outside it. That is how a SwiftUI view, which is an immutable struct, can still change its @State: the state is stored outside the view, and the setter is nonmutating.

Step 7

Build a small Binding

Suppose another piece of code needs to change the username. If you pass it preferences.username, it receives a copy of the string — changing the copy does nothing. What you want to pass is access: a way to read the value and a way to write it. Two closures do that:

Add to Wrappers.swift
struct Ref<Value> {
    let get: () -> Value
    let set: (Value) -> Void

    var value: Value {
        get { get() }
        nonmutating set { set(newValue) }
    }
}

Now make Stored hand one out as its projected value. Replace Stored with this version — the addition is the projectedValue property at the end:

In Wrappers.swift, replace Stored with this
@propertyWrapper
struct Stored<Value> {
    private let key: String
    private let defaultValue: Value

    init(wrappedValue: Value, _ key: String) {
        self.key = key
        self.defaultValue = wrappedValue
    }

    var wrappedValue: Value {
        get { UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue }
        nonmutating set { UserDefaults.standard.set(newValue, forKey: key) }
    }

    var projectedValue: Ref<Value> {
        Ref(get: { wrappedValue }, set: { wrappedValue = $0 })
    }
}

And write a function that changes a string it does not own:

Add to Wrappers.swift
func shout(_ name: Ref<String>) {
    name.value = name.value.uppercased()
}
In main.swift, replace everything below import Foundation with
UserDefaults.standard.removeObject(forKey: "username")

let preferences = Preferences()
preferences.username = "andrew"

shout(preferences.$username)           // hand over read-write access, not a copy
print(preferences.username)

Run it (⌘R). The console shows

Console
ANDREW
Program ended with exit code: 0

shout changed the stored username without knowing anything about Preferences or UserDefaults. It was handed $username — the ability to read and write — and that was enough.

You have just built Binding. SwiftUI's version holds a getter and a setter, exactly like your Ref, and a parent view passes $something to a child view for exactly this reason.

Step 8

Compare with Apple's version

You can read Apple's Binding and see that it has the same parts. Make one more file — File ▸ New ▸ File from Template… (⌘N), Swift File, named Peek — and replace everything in it with this small SwiftUI view:

Peek.swift — the whole file
import SwiftUI

struct Counter: View {
    @State private var count = 0
    @Binding var name: String

    var body: some View {
        Text("\(name): \(count)")
    }
}

Choose Product ▸ Build (⌘B) to check that it compiles. Now click on the word Binding and choose Navigate ▸ Jump to Definition (⌃⌘J). Xcode opens SwiftUI's interface at this declaration (shortened here):

From Apple’s SwiftUI interface (shortened) — not code to type
@frozen @propertyWrapper @dynamicMemberLookup public struct Binding<Value> {
  public var wrappedValue: Value {
    get
    nonmutating set
  }
  public var projectedValue: Binding<Value> {
    get
  }
}

Every part of it is something you wrote today: @propertyWrapper, a wrappedValue with a nonmutating set, and a projectedValue. (A binding's projected value is itself, which is why you can write $name on a @Binding property and pass it further down.)

One more thing to look at. In Xcode 27, @State is a macro: a piece of code that writes code. Xcode can show you what it wrote. Go back to Peek.swift, click on @State, and choose Editor ▸ Expand Macro. Among the generated code you will find these lines:

From Xcode’s expansion of the macro (shortened) — not code to type
// the hidden storage:
private var __count = SwiftUICore.State._makeStorage({ 0 })

// `count` itself is given these:
get { __count.wrappedValue }
nonmutating set { __count.wrappedValue = newValue }

// and `$count` is given this:
get { __count.projectedValue }

It is the hand-written version from step 3 again. There is hidden storage, here named __count. count has become a computed property that reads and writes the storage's wrappedValue, and its setter is nonmutating — the same trick as your Stored in step 6. And $count returns the storage's projectedValue.

What you built today

Tomorrow you start on TinyUI, a miniature SwiftUI that draws with text: things that can draw themselves, and the difference between some and any.

The finished project

Stuck, or want to compare with your own? This is the project as it stands at the end of today, built and run before it was put here.

Flash cards

13 cards for today, each labelled with its topic. 7 ask how you would write something; the other 6 are trick questions, written to catch a misunderstanding. Answer in your head first, then tap the card.

Syntax · Property wrappersDeclare a property wrapper.
@propertyWrapper
struct Uppercased {
    private var value = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.uppercased() }
    }
}

Mark the type @propertyWrapper and give it a wrappedValue property. That is the minimum.

Syntax · Property wrappersAttach a wrapper to a property, with an argument and a starting value.
struct Mixer {
    @Clamped(0...10) var level = 5
}

The argument goes in parentheses after the wrapper's name. The starting value goes after =, as usual.

Syntax · Property wrappersWrite the initializer that makes @AtLeast(1) var quantity = 3 work.
@propertyWrapper
struct AtLeast {
    private var value: Int
    private let minimum: Int

    init(wrappedValue: Int, _ minimum: Int) {
        self.minimum = minimum
        self.value = max(wrappedValue, minimum)
    }

    var wrappedValue: Int {
        get { value }
        set { value = max(newValue, minimum) }
    }
}

struct Cart {
    @AtLeast(1) var quantity = 3
}

The first parameter must be named wrappedValue: it receives the = 3. The arguments in parentheses come after it.

Syntax · Projected valuesRead a wrapper's projected value.
var mixer = Mixer()
mixer.level = 42
let wasClamped = mixer.$level
print(wasClamped)

Put $ in front of the property name. You get the wrapper's projectedValue — here a Bool, and it prints true.

Syntax · Property wrappersWrite a setter that does not mutate the struct it belongs to.
@propertyWrapper
struct Remembered {
    var wrappedValue: Int {
        get { UserDefaults.standard.integer(forKey: "remembered") }
        nonmutating set { UserDefaults.standard.set(newValue, forKey: "remembered") }
    }
}

Write nonmutating set. Use it when the value is stored somewhere outside the struct.

Syntax · Property wrappersFrom inside a type, reach the wrapper itself rather than the value it wraps.
struct Mixer {
    @Clamped(0...10) var level = 5

    func describe() -> String {
        "level \(level), clamped: \(_level.projectedValue)"
    }
}

The wrapper is stored in a property with a leading underscore: _level. It is private to the type that declares it.

Syntax · Projected valuesPass read-and-write access to a property into a function.
func reset(_ name: Ref<String>) {
    name.value = "guest"
}

func signOut(_ preferences: Preferences) {
    reset(preferences.$username)
}

Pass the projected value, $username. The function receives a way to read and write the original, not a copy of it. In SwiftUI the same idea is called Binding.

Trick question · Property wrappers@Clamped(0...10) var level = 50 — right after the struct is created, is level 50 or 10?

10. The starting value does not skip the wrapper. It is passed to init(wrappedValue:), and the initializer you wrote clamps it.

Trick question · Projected valuesDoes $level always give you a Binding?

No. $ gives you whatever the wrapper's projectedValue is. For your Clamped it is a Bool; for SwiftUI's @State it is a Binding. A wrapper with no projectedValue has no $ form at all. Using one on ClampedToTen does not compile: value of type 'SimpleVolume' has no member '$level'

Trick question · nonmutatinglet preferences = Preferences() is a constant. Does preferences.username = "andrew" compile?

Yes — and it prints andrew. The setter is nonmutating: the value lives in UserDefaults, not in the struct, so nothing inside the constant changes. It is the same reason a SwiftUI view, which is an immutable struct, can change its @State.

Trick question · Property wrappersTwo Mixer values each use @Clamped. Two Preferences values each use @Stored("username"). Which pair shares its data?

Only the Preferences pair. Each Mixer holds its own wrapper with its own value, so the two are independent (they print 9 5). Both Preferences read and write the same UserDefaults key, so they see the same value (andrew andrew). A wrapper shares data only when it stores that data somewhere outside itself.

Trick question · Property wrappersCan you put a property wrapper on a let?

No. property wrapper can only be applied to a 'var' A wrapped property is really a computed property backed by hidden storage, and a computed property cannot be a let.

Trick question · Projected valuesYou set mixer.level = 42, then mixer.level = 7. What is mixer.$level now?

false. Your projected value describes only the most recent write, and 7 was in range. A projected value is ordinary state that you design: it means whatever you make it mean.

Further reading