What are property wrappers, and how do you write one?
TL;DR
A property wrapper moves repeated property logic into one reusable type.
You mark a type with @propertyWrapper and give it a wrappedValue. Then you use it as @Name var x.
It can also expose a projectedValue, which you read with $x. SwiftUI's @State and @Binding are property wrappers.
The problem it solves
Without a wrapper, you repeat the same logic for many properties:
struct Player {
private var _health = 100
var health: Int {
get { _health }
set { _health = min(max(newValue, 0), 100) }
}
}Imagine this for ten properties.
Writing a wrapper
@propertyWrapper
struct Clamped {
private var value: Int
private let range: ClosedRange<Int>
init(wrappedValue: Int, _ range: ClosedRange<Int>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: Int {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}
struct Player {
@Clamped(0...100) var health = 100
@Clamped(0...10) var level = 1
}
var p = Player()
p.health = 150
print(p.health) // 100What the compiler generates
// You write:
@Clamped(0...100) var health = 100
// Swift creates roughly:
private var _health = Clamped(wrappedValue: 100, 0...100)
var health: Int {
get { _health.wrappedValue }
set { _health.wrappedValue = newValue }
}_health is the wrapper itself. It is private to the type.
Projected value: $name
A wrapper can expose extra data through projectedValue.
@propertyWrapper
struct Trimmed {
private(set) var projectedValue = false // true if trimming changed the text
private var value = ""
init(wrappedValue: String) { self.wrappedValue = wrappedValue }
var wrappedValue: String {
get { value }
set {
let trimmed = newValue.trimmingCharacters(in: .whitespaces)
projectedValue = trimmed != newValue
value = trimmed
}
}
}
struct Form {
@Trimmed var email = ""
}
var f = Form()
f.email = " a@b.com "
print(f.email) // "a@b.com"
print(f.$email) // trueIn SwiftUI, $text on a @State gives a Binding<String>. That is its projected value.
Wrappers you already use
| Wrapper | Where | What it does |
|---|---|---|
@State | SwiftUI | View-owned state |
@Binding | SwiftUI | Read/write reference to someone else's state |
@Environment | SwiftUI | Read a value from the environment |
@AppStorage | SwiftUI | Read/write UserDefaults |
@Published | Combine | Emits changes in an ObservableObject |
Interviewers sometimes check that you know @MainActor and @Observable are not property wrappers. @MainActor is a global actor. @Observable is a macro.
Where you can use them
- Stored properties in structs and classes.
- Local variables (Swift 5.4+).
- Function and closure parameters (Swift 5.5+).
You cannot use a wrapper on a computed property. You cannot declare one in a protocol requirement.
Limits and gotchas
- A wrapped property cannot be
lazy,weak, orunowned. @Publishedworks only inside classes.- The wrapper is not thread-safe by default. A wrapper with a lock inside does not make
x += 1atomic, because that is a read followed by a write. - Wrappers can hide logic. Keep them small and obvious.
Common follow-up questions
- What is the difference between a property wrapper and a macro? A wrapper is a runtime type that stores the value. A macro generates code at compile time.
@Observablereplaced@Publishedstyle wrappers for observation. - Can a wrapper access the object that contains it? Not through the public API. There is an underscored static subscript (
_enclosingInstance) that@Publisheduses, but it is not official. - Why use
init(wrappedValue:)? It lets users write@Clamped(0...100) var health = 100with a normal default value. - How is
@Statedifferent from a normal wrapper? SwiftUI stores the real value outside the view struct, so it survives when the view is recreated. - Can you nest wrappers? Yes, like
@A @B var x. It works, but it is hard to read, so it is rare.