Custom Property Wrappers
Reviewed & published by Brayan K
Bundle reusable get/set behaviour into a type with @propertyWrapper. Learn the required wrappedValue, the projectedValue behind the $ prefix, custom initializers, and why @State, @Published, and @Binding are all property wrappers.
Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • Define a wrapper with @propertyWrapper
- • Provide the required wrappedValue computed property
- • Publish a projectedValue via the $ prefix
- • Write your own (Clamped, Trimmed, UserDefault)
- • Use init(wrappedValue:) for default values
- • See why @State / @Published / @Binding are wrappers
1️⃣ Your First Wrapper
Mark a type @propertyWrapper and give it a wrappedValue. Making wrappedValue a computed property lets you run logic — here, clamping — on every write.
// A property wrapper is a type marked @propertyWrapper that adds
// reusable behaviour around a stored value. It MUST expose wrappedValue.
@propertyWrapper
struct Clamped {
private var value: Int
private let range: ClosedRange<Int>
// wrappedValue is what you read/write through the wrapped property.
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 Volume {
@Clamped(0...10) var level: Int = 5
}
var v = Volume()
v.level = 99 // clamped back into range
print(v.level) // 102️⃣ projectedValue and the $ Prefix
A wrapper can publish an extra value via projectedValue. You reach it with the $ prefix on the wrapped property — separate from wrappedValue.
// projectedValue is an EXTRA value the wrapper publishes.
// You reach it with the $ prefix on the wrapped property.
@propertyWrapper
struct Trimmed {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespaces) }
}
// The $ prefix will expose this: did the last write contain spaces?
var projectedValue: Bool { value.count != wrappedValue.count }
init(wrappedValue: String) { self.wrappedValue = wrappedValue }
}
struct Form {
@Trimmed var name: String = " Ada "
}
var f = Form()
print(f.name) // "Ada" (wrappedValue)
print(f.$name) // false (projectedValue)3️⃣ A Practical Wrapper: UserDefault
Because a wrapper is a normal type, it can store state and reach external systems. This generic UserDefault persists each write automatically.
// A practical wrapper: persist a value in UserDefaults automatically.
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
struct Settings {
@UserDefault(key: "isDarkMode", defaultValue: false)
var isDarkMode: Bool
}
var s = Settings()
s.isDarkMode = true // written straight to UserDefaults
print(s.isDarkMode) // trueYour turn. Fill in the defining attribute and the required property name.
// 🎯 YOUR TURN — fill in each ___ then run it on SwiftFiddle.
___ // 👉 the attribute that defines a wrapper
struct NonNegative {
private var value: Int = 0
var ___: Int { // 👉 the required property name
get { value }
set { value = max(0, newValue) }
}
init(wrappedValue: Int) { self.wrappedValue = wrappedValue }
}
struct Account {
@NonNegative var balance: Int = 0
}
var a = Account()
a.balance = -50
print(a.balance) // 0
// ✅ Expected output:
// 04️⃣ Wrappers You Already Use
SwiftUI's state tools are property wrappers. @State owns value-type state and projects a Binding through $; @Binding shares a source of truth; @Published projects a Combine publisher.
// SwiftUI's own state tools are ALL property wrappers.
import SwiftUI
struct CounterView: View {
@State private var count = 0 // view-owned mutable state
var body: some View {
VStack {
Text("Count: \(count)")
// $count is the projectedValue: a Binding<Int>
Stepper("Adjust", value: $count)
ChildView(count: $count) // pass the binding down
}
}
}
struct ChildView: View {
@Binding var count: Int // shares the parent's source of truth
var body: some View { Text("Child sees \(count)") }
}Pro Tips
- 💡 Make wrappedValue computed when you need logic on every read or write.
- 💡 Add init(wrappedValue:) so default-value syntax just works.
- 💡 Use projectedValue to expose extras — like SwiftUI's $ bindings.
- 💡 Make wrappers generic (UserDefault<T>) to reuse them across types.
Common Errors (and the fix)
- "Property wrapper type ... has no member 'wrappedValue'": you forgot to add wrappedValue.
- $name not available: you didn't define projectedValue on the wrapper.
- Default value won't compile: add an init(wrappedValue:).
- Using @State outside a View: SwiftUI wrappers only work inside SwiftUI types.
📋 Quick Reference
| Task | Code | Effect |
|---|---|---|
| Define wrapper | @propertyWrapper struct W | declares a wrapper |
| Required member | var wrappedValue | the visible value |
| Projection | var projectedValue | reached via $ |
| Default value | init(wrappedValue:) | enables = syntax |
| Apply it | @W var x | wraps property x |
| SwiftUI binding | $count | projectedValue |
Mini-Challenge: Capitalized Wrapper
Write a wrapper that capitalizes every word it stores, then apply it to a title property.
// 🎯 MINI-CHALLENGE: Capitalized wrapper
// 1. Write a @propertyWrapper struct named Capitalized.
// 2. Its wrappedValue is a String; on set, store newValue.capitalized.
// 3. Add init(wrappedValue:) that routes through the setter.
// 4. Use it: @Capitalized var title = "hello world"
// then print(title) -> "Hello World"
//
// ✅ Expected output:
// Hello World
// your code here
Practice quiz
Which attribute marks a type as a property wrapper?
- @propertyWrapper
- @property
- @wrapped
- @wrapper
Answer: @propertyWrapper. A type becomes a property wrapper when you mark it with the @propertyWrapper attribute.
Which member is REQUIRED for a property wrapper to compile?
- init()
- description
- wrappedValue
- projectedValue
Answer: wrappedValue. Every property wrapper must expose a wrappedValue property; everything else is optional.
When you write @Clamped var x, what is x's type as seen by the user?
- Always Int
- The type of wrappedValue
- Optional
- The wrapper type
Answer: The type of wrappedValue. Accessing the wrapped property reads and writes through wrappedValue, so x has wrappedValue's type.
What does the $ prefix on a wrapped property give you?
- The raw storage
- A force-unwrap
- The wrapper's init
- The projectedValue
Answer: The projectedValue. The $ prefix exposes the wrapper's projectedValue, an extra value the wrapper chooses to publish.
In SwiftUI, what does @State actually provide under the hood?
- A property wrapper managing view-owned mutable state
- A computed property
- A singleton
- A global variable
Answer: A property wrapper managing view-owned mutable state. @State is a property wrapper that stores value-type state owned and tracked by the view.
For @Binding var name, what does $name give you in SwiftUI?
- A new copy
- A Binding<String> projectedValue
- Nothing
- A String
Answer: A Binding<String> projectedValue. @Binding projects a Binding so child views can read and write the source of truth via $name.
How does a wrapper run code when a value is assigned?
- Using a global hook
- With a deinit
- It cannot
- By making wrappedValue a computed property with a setter
Answer: By making wrappedValue a computed property with a setter. Defining wrappedValue as a computed property lets the get and set run custom logic on every access.
Which initializer does the compiler call for @Wrapper var v = 5?
- init(projectedValue:)
- init(value:)
- init(wrappedValue:)
- init()
Answer: init(wrappedValue:). An assigned default value is forwarded to init(wrappedValue:) when that initializer exists.
What is @Published from Combine an example of?
- A result builder
- A property wrapper that announces changes via a publisher
- A macro
- A protocol
Answer: A property wrapper that announces changes via a publisher. @Published is a property wrapper whose projectedValue is a Combine publisher that emits on change.
Can a property wrapper add stored properties of its own?
- Yes, it is a normal type that can store state
- Only with macros
- No, never
- Only computed ones
Answer: Yes, it is a normal type that can store state. A wrapper is an ordinary struct or class, so it can hold its own stored properties for backing storage.
Continue this course
- Previous: App Architecture with MVVM
- Next: The Swift Testing Framework — Modern testing with @Test, #expect, parameterized tests and suites
Frequently asked questions
What is a property wrapper in Swift?
A property wrapper is a type you mark with the @propertyWrapper attribute that bundles reusable logic around a stored property. Instead of repeating the same get/set behaviour on many properties, you write it once in the wrapper. The single requirement is that the wrapper exposes a wrappedValue property; reading and writing the wrapped property actually reads and writes through wrappedValue. Common uses include clamping numbers to a range, trimming strings, or persisting values to UserDefaults.
What is wrappedValue and why is it required?
wrappedValue is the actual value the outside world sees when it uses the wrapped property. When you write @Clamped var level: Int, accessing level reads wrappedValue and assigning to it writes wrappedValue. Because it is the bridge between the property and the wrapper's behaviour, every property wrapper must provide it. Making wrappedValue a computed property with a custom get and set is exactly how a wrapper runs code on every access.
What is projectedValue and the $ prefix?
projectedValue is an optional extra value a wrapper can publish alongside wrappedValue. You access it by putting a dollar sign before the property name. For example, SwiftUI's @State projects a Binding, so $count gives you a Binding<Int> you can pass to child views. Combine's @Published projects a publisher. If a wrapper does not define projectedValue, the $ prefix simply is not available for that property.
How do property wrapper initializers work?
When you write a default value like @Clamped(0...10) var level: Int = 5, the compiler synthesizes a call to the wrapper's init(wrappedValue:) initializer, passing 5 as wrappedValue and 0...10 as the other argument. You can define several initializers, but init(wrappedValue:) is special because it receives the assigned default. This is why custom wrappers usually provide an init(wrappedValue:) so they integrate naturally with normal property syntax.
Are SwiftUI's @State and @Binding property wrappers?
Yes. @State, @Binding, @Published, @Environment, and @StateObject are all property wrappers built into Apple's frameworks. @State is a wrapper that stores view-owned value-type state and projects a Binding through the $ prefix. @Binding wraps a reference to a source of truth that lives elsewhere so a child view can read and write it. Understanding the wrapper mechanism demystifies why $ produces a binding and why these annotations behave the way they do.