Swift Property Wrappers
A property wrapper adds a layer of logic around how a property stores and retrieves its value. Instead of writing the same validation or transformation code in every struct or class, you define it once in a property wrapper and reuse it everywhere.
The Smart Outlet Analogy
┌──────────────────────────────────────────────────┐
│ Property Wrapper = A Smart Power Outlet │
│ │
│ Regular outlet: │
│ Plug in device → device gets power (raw) │
│ │
│ Smart outlet: │
│ Plug in device → outlet checks voltage, │
│ regulates it, then delivers safe power │
│ │
│ The device does not need to worry about │
│ voltage — the outlet handles it automatically. │
└──────────────────────────────────────────────────┘
The Problem Without Property Wrappers
struct UserProfile {
private var _age: Int = 0
var age: Int {
get { return _age }
set { _age = max(0, newValue) } // validation repeated everywhere
}
private var _username: String = ""
var username: String {
get { return _username }
set { _username = newValue.lowercased() } // same pattern again
}
}
The same wrapping pattern appears for every property. Property wrappers eliminate this repetition.
Creating a Property Wrapper
@propertyWrapper
struct Clamped {
private var value: Int
private 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)
}
}
The @propertyWrapper attribute marks a struct as a wrapper. The required wrappedValue property is the actual value the wrapper manages.
Using the Wrapper
struct GamePlayer {
@Clamped(0...100) var health: Int = 100
@Clamped(1...10) var level: Int = 1
}
var player = GamePlayer()
player.health = 150 // clamped to 100
player.health = -30 // clamped to 0
player.level = 15 // clamped to 10
print(player.health) // 0
print(player.level) // 10
┌──────────────────────────────────────────────────┐
│ @Clamped in action │
│ │
│ health = 150 → range 0...100 → stored: 100 │
│ health = -30 → range 0...100 → stored: 0 │
│ level = 15 → range 1...10 → stored: 10 │
└──────────────────────────────────────────────────┘
A String Trimming Wrapper
@propertyWrapper
struct Trimmed {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespaces) }
}
}
struct SignupForm {
@Trimmed var username: String = ""
@Trimmed var email: String = ""
}
var form = SignupForm()
form.username = " swift_user "
form.email = " user@mail.com "
print(form.username) // swift_user
print(form.email) // user@mail.com
Whitespace is stripped automatically every time a value is set. The form struct stays clean with no manual trimming calls anywhere.
Projected Value – Extra Information from a Wrapper
@propertyWrapper
struct Logged<T> {
private var value: T
private(set) var projectedValue: [T] = []
var wrappedValue: T {
get { value }
set {
projectedValue.append(newValue)
value = newValue
}
}
init(wrappedValue: T) {
self.value = wrappedValue
}
}
struct Thermometer {
@Logged var temperature: Double = 20.0
}
var t = Thermometer()
t.temperature = 22.5
t.temperature = 19.0
t.temperature = 24.0
print(t.temperature) // 24.0 ← current value
print(t.$temperature) // [22.5, 19.0, 24.0] ← history via $
The projectedValue is accessed with a $ prefix. This gives you a second, parallel way to read data from the wrapper — here, a full history of changes.
Built-in SwiftUI Property Wrappers
| Wrapper | Purpose |
|---|---|
| @State | Local view state that triggers UI refresh |
| @Binding | Two-way connection to a parent's state |
| @ObservedObject | Watch an external observable object |
| @EnvironmentObject | Share data across the entire view hierarchy |
| @AppStorage | Sync a property with UserDefaults |
SwiftUI is built on property wrappers. Understanding how they work makes reading and writing SwiftUI code much more intuitive.
