Mastering Swift Property Wrappers
Mastering Swift Property Wrappers
Property wrappers are one of Swift's most powerful features, allowing you to add behavior to properties in a reusable way. Let's explore how they work and when to use them.
What are Property Wrappers?
Property wrappers let you separate property storage and behavior logic. They're defined with the @propertyWrapper attribute and can be applied to any property.
@propertyWrapper
struct Capitalized {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.capitalized }
}
}
struct User {
@Capitalized var name: String
}
var user = User()
user.name = "john doe"
print(user.name) // "John Doe"
Common Use Cases
UserDefaults Storage
@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: "theme", defaultValue: "light")
var theme: String
@UserDefault(key: "notifications", defaultValue: true)
var notificationsEnabled: Bool
}
Validation
@propertyWrapper
struct Clamped<Value: Comparable> {
private var value: Value
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 Game {
@Clamped(0...100) var health: Int = 100
@Clamped(0...10) var level: Int = 1
}
Thread Safety
@propertyWrapper
struct Atomic<Value> {
private let queue = DispatchQueue(label: "atomic.queue")
private var value: Value
var wrappedValue: Value {
get {
queue.sync { value }
}
set {
queue.sync { value = newValue }
}
}
init(wrappedValue: Value) {
self.value = wrappedValue
}
}
class Counter {
@Atomic var count: Int = 0
}
SwiftUI Property Wrappers
SwiftUI heavily relies on property wrappers:
@State
struct CounterView: View {
@State private var count = 0
var body: some View {
Button("Count: \(count)") {
count += 1
}
}
}
@Binding
struct ChildView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
@ObservedObject
class ViewModel: ObservableObject {
@Published var text = ""
}
struct ContentView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
TextField("Enter text", text: $viewModel.text)
}
}
Advanced Features
Projected Value
Property wrappers can expose a projected value using $:
@propertyWrapper
struct Tracked<Value> {
private var value: Value
private(set) var projectedValue: Int = 0
var wrappedValue: Value {
get { value }
set {
value = newValue
projectedValue += 1
}
}
init(wrappedValue: Value) {
self.value = wrappedValue
}
}
struct Example {
@Tracked var name: String = ""
}
var example = Example()
example.name = "John"
example.name = "Jane"
print(example.$name) // 2 (number of times changed)
Best Practices
- Keep it simple - Property wrappers should do one thing well
- Document behavior - Make side effects clear
- Consider composition - Combine multiple wrappers when needed
- Use generics - Make wrappers reusable with generic types
- Test thoroughly - Wrappers can hide complexity
Conclusion
Property wrappers are a powerful tool for creating cleaner, more maintainable Swift code. They reduce boilerplate and make common patterns reusable across your codebase.