Property wrappers are a Swift feature that allows you to define reusable logic for getting and setting property values. They reduce boilerplate code and enable elegant solutions for common patterns like lazy initialization, validation, and persistence. This post is a deep dive suited for intermediate to advanced Swift developers who already have basic familiarity with property wrappers.
What are Property Wrappers?
Property wrappers add a layer of separation between code that manages how a property is stored and the code that defines a property. They're defined using the @propertyWrapper attribute.
Creating a Simple Property Wrapper
Here's a basic example that ensures a value stays within bounds:
@propertyWrapper
struct Clamped<Value: Comparable> {
private var value: Value
private let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = min(max(range.lowerBound, wrappedValue), range.upperBound)
}
}
struct GameCharacter {
@Clamped(0...100) var health: Int = 100
@Clamped(0...100) var mana: Int = 50
}
var character = GameCharacter()
character.health = 150 // Clamped to 100
character.mana = -10 // Clamped to 0
Projected Values
Property wrappers can provide additional functionality through projected values:
@propertyWrapper
struct Capitalized {
private var value: String
var wrappedValue: String {
get { value }
set { value = newValue }
}
var projectedValue: String {
value.uppercased()
}
init(wrappedValue: String) {
self.value = wrappedValue.capitalized
}
}
struct User {
@Capitalized var name: String
}
var user = User(name: "john doe")
print(user.name) // "John Doe"
print(user.$name) // "JOHN DOE" (projected value)
UserDefaults Property Wrapper
A practical example for persisting values:
@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: "")
var username: String
@UserDefault(key: "fontSize", defaultValue: 16.0)
var fontSize: Double
}
var settings = AppSettings()
settings.isDarkMode = true // Automatically persisted
Validation Property Wrapper
Validate property values on assignment:
@propertyWrapper
struct Validated<Value> {
private var value: Value
private let validator: (Value) -> Bool
var wrappedValue: Value {
get { value }
set {
guard validator(newValue) else {
print("Validation failed")
return
}
value = newValue
}
}
init(wrappedValue: Value, validator: @escaping (Value) -> Bool) {
self.validator = validator
self.value = wrappedValue
}
}
struct EmailForm {
@Validated(validator: { $0.contains("@") && $0.contains(".") })
var email: String = ""
}
var form = EmailForm()
form.email = "invalid" // Validation failed
form.email = "user@example.com" // Valid
Combining Property Wrappers
You can stack multiple property wrappers:
@propertyWrapper
struct Trimmed {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
}
struct Form {
@UserDefault(key: "name", defaultValue: "")
@Trimmed
var name: String
}
SwiftUI Built-in Property Wrappers
SwiftUI extensively uses property wrappers:
-
@State- Local state management -
@Binding- Two-way bindings -
@ObservedObject- Observable object references -
@EnvironmentObject- Environment-provided objects -
@Environment- Environment values
Best Practices
-
Keep wrappers focused - Each wrapper should handle one responsibility
-
Document behavior - Make it clear what the wrapper does
-
Consider performance - Avoid heavy operations in getters/setters
-
Provide good defaults - Make the wrapper easy to use
-
Use projected values - For additional related functionality
Wrap up
Property wrappers are a powerful tool for reducing boilerplate and creating reusable property behavior. They enable clean, declarative code and are fundamental to SwiftUI's design.
Resources: