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

  1. Keep it simple - Property wrappers should do one thing well
  2. Document behavior - Make side effects clear
  3. Consider composition - Combine multiple wrappers when needed
  4. Use generics - Make wrappers reusable with generic types
  5. 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.