Swift Property Wrappers
A property wrapper adds a layer of logic between a property and the code that stores or reads it. Instead of repeating validation, clamping, or storage logic in every property, you write it once in a wrapper and reuse it anywhere. Property wrappers power SwiftUI's @State, @Binding, @Published, and many other common tools.
The Problem They Solve
Suppose every temperature property in your app must stay between -50 and 150. Without a wrapper, you repeat the clamping logic everywhere:
struct Weather {
private var _temperature: Double = 0
var temperature: Double {
get { _temperature }
set { _temperature = max(-50, min(150, newValue)) }
}
}
struct Engine {
private var _heatLevel: Double = 0
var heatLevel: Double {
get { _heatLevel }
set { _heatLevel = max(-50, min(150, newValue)) }
}
}
// Same logic written twice — and growingCreating a Property Wrapper
Mark a struct, class, or enum with @propertyWrapper. Implement a computed property named exactly wrappedValue — that is the actual storage or logic gate.
@propertyWrapper
struct Clamped {
private var value: Double
let range: ClosedRange<Double>
init(wrappedValue: Double, range: ClosedRange<Double>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: Double {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}Using the Wrapper
struct Weather {
@Clamped(range: -50...150) var temperature: Double = 20
}
var w = Weather()
print(w.temperature) // Output: 20.0
w.temperature = 200 // Over the maximum
print(w.temperature) // Output: 150.0
w.temperature = -100 // Under the minimum
print(w.temperature) // Output: -50.0Diagram: Property Wrapper Flow
Code writes: w.temperature = 200
|
Clamped.wrappedValue.set(200)
|
min(max(200, -50), 150) = 150
|
Stored value = 150
Code reads: w.temperature
|
Clamped.wrappedValue.get()
|
Returns 150
A Capitalized Wrapper
@propertyWrapper
struct Capitalized {
private var text = ""
var wrappedValue: String {
get { text }
set { text = newValue.capitalized }
}
}
struct UserProfile {
@Capitalized var firstName: String
@Capitalized var lastName: String
}
var profile = UserProfile()
profile.firstName = "alice"
profile.lastName = "johnson"
print(profile.firstName) // Output: Alice
print(profile.lastName) // Output: JohnsonprojectedValue — The $ Accessor
A wrapper can expose a second value through a projectedValue property. Access it with a $ prefix. SwiftUI's @State uses this to expose a Binding.
@propertyWrapper
struct Validated {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue }
}
var projectedValue: Bool {
return value.count >= 6
}
}
struct LoginForm {
@Validated var password: String
}
var form = LoginForm()
form.password = "abc"
print(form.$password) // Output: false (less than 6 chars)
form.password = "securepass"
print(form.$password) // Output: trueCommon Built-In Property Wrappers
| Wrapper | Where Used | What It Does |
|---|---|---|
@State | SwiftUI | Local view state; triggers re-render on change |
@Binding | SwiftUI | Two-way connection to a parent's state |
@Published | Combine | Publishes changes to subscribers |
@Environment | SwiftUI | Reads values injected from the environment |
@AppStorage | SwiftUI | Persists values in UserDefaults automatically |
@ObservedObject | SwiftUI | Watches an external observable object for changes |
UserDefaults Wrapper Example
A practical wrapper that saves and loads values from 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 AppSettings {
@UserDefault(key: "isDarkMode", defaultValue: false)
var isDarkMode: Bool
@UserDefault(key: "username", defaultValue: "Guest")
var username: String
}
var settings = AppSettings()
print(settings.username) // Output: Guest
settings.username = "Alice"
print(settings.username) // Output: Alice (saved to UserDefaults)Wrapper Initialization Rules
With a Default Value
struct Foo {
@Clamped(range: 0...100) var score: Double = 50
// Calls: Clamped(wrappedValue: 50, range: 0...100)
}Without a Default — Set in init
struct Bar {
@Clamped(range: 0...100) var score: Double
init(score: Double) {
self._score = Clamped(wrappedValue: score, range: 0...100)
}
}Access the wrapper itself (not the wrapped value) through the underscore-prefixed name: _score.
Summary
Property wrappers encapsulate reusable property logic — validation, clamping, formatting, persistence — behind the @ annotation syntax. Define them with @propertyWrapper and a wrappedValue computed property. Add a projectedValue to expose extra information via the $ accessor. SwiftUI's entire state management system is built on property wrappers, making this a foundational concept for iOS development.
