Skip to content

Property Wrappers

Property wrappers (Swift 5.1+) encapsulate property behavior, reducing boilerplate for common patterns like lazy initialization or validation. They are used extensively in SwiftUI (e.g., @State, @Published).

Defining a Property Wrapper

Use @propertyWrapper to define reusable logic.

Example: Basic Clamping:

swift
@propertyWrapper
struct Clamped<T: Comparable> {
    private var value: T
    let min: T
    let max: T
    
    init(wrappedValue: T, min: T, max: T) {
        self.min = min
        self.max = max
        self.value = Swift.max(min, Swift.min(max, wrappedValue))
    }
    
    var wrappedValue: T {
        get { value }
        set { value = Swift.max(min, Swift.min(max, newValue)) }
    }
}

struct Game {
    @Clamped(min: 0, max: 100) var score: Int
}

var game = Game(score: 150)
print(game.score) // 100
game.score = -10
print(game.score) // 0

Projected Values

Expose additional functionality with $.

Example: User Defaults Wrapper:

swift
@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    private let defaults = UserDefaults.standard
    
    var wrappedValue: T {
        get { defaults.object(forKey: key) as? T ?? defaultValue }
        set { defaults.set(newValue, forKey: key) }
    }
    
    var projectedValue: Bool {
        return defaults.object(forKey: key) != nil
    }
}

struct Settings {
    @UserDefault(key: "theme", defaultValue: "light") var theme: String
}

var settings = Settings()
print(settings.theme) // "light"
print(settings.$theme) // false
settings.theme = "dark"
print(settings.$theme) // true

Property Wrapper Composition

Combine multiple wrappers for layered behavior.

Example:

swift
@propertyWrapper
struct Logged<T> {
    private var value: T
    init(wrappedValue: T) {
        self.value = wrappedValue
    }
    var wrappedValue: T {
        get { value }
        set {
            print("Setting \(newValue)")
            value = newValue
        }
    }
}

struct Config {
    @Logged @Clamped(min: 1, max: 10) var level: Int
}

var config = Config(level: 5)
config.level = 12 // "Setting 10"
print(config.level) // 10

Initializer Injection

Pass parameters to wrappers via custom initializers.

Example:

swift
@propertyWrapper
struct ValidatedEmail {
    private var value: String
    private let regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
    
    init(wrappedValue: String) {
        self.value = isValid(wrappedValue) ? wrappedValue : ""
    }
    
    var wrappedValue: String {
        get { value }
        set { value = isValid(newValue) ? newValue : value }
    }
    
    private func isValid(_ email: String) -> Bool {
        return email.matches(of: regex).count == 1
    }
}

struct Profile {
    @ValidatedEmail var email: String
}

var profile = Profile(email: "alice@example.com")
print(profile.email) // "alice@example.com"
profile.email = "invalid"
print(profile.email) // "alice@example.com"

Thread Safety

Ensure wrappers are thread-safe for concurrent access.

Example:

swift
@propertyWrapper
actor SafeValue<T> {
    private var value: T
    
    init(wrappedValue: T) {
        self.value = wrappedValue
    }
    
    var wrappedValue: T {
        get async { await value }
        set async { value = newValue }
    }
}

struct SharedData {
    @SafeValue var counter: Int
}

let data = SharedData(counter: 0)
Task {
    await data.$counter.increment()
}

Best Practices

  • Focused Logic: Keep wrappers simple and single-purpose.
  • Projected Values: Use for metadata or control flags.
  • Composition: Combine wrappers for complex behavior.
  • Thread Safety: Use actors or locks for concurrency.
  • Documentation: Clearly document wrapper behavior.
  • Testing: Verify wrapper logic with unit tests.

Troubleshooting

  • Initialization Errors: Ensure wrapper init matches usage.
  • Thread Safety Issues: Use actors for concurrent access.
  • Composition Conflicts: Order wrappers carefully.
  • Performance: Profile wrapper overhead for critical paths.
  • Regex Validation: Test with diverse inputs.

Example: Comprehensive Property Wrapper Usage

swift
@propertyWrapper
struct Tracked<T: Codable> {
    private var value: T
    private let key: String
    private let storage = UserDefaults.standard
    
    init(wrappedValue: T, key: String) {
        self.key = key
        if let data = storage.data(forKey: key),
           let decoded = try? JSONDecoder().decode(T.self, from: data) {
            self.value = decoded
        } else {
            self.value = wrappedValue
        }
    }
    
    var wrappedValue: T {
        get { value }
        set {
            value = newValue
            if let encoded = try? JSONEncoder().encode(value) {
                storage.set(encoded, forKey: key)
            }
        }
    }
    
    var projectedValue: Bool {
        return storage.data(forKey: key) != nil
    }
}

struct AppConfig {
    @Tracked(key: "userProfile") var profile: User?
    @Clamped(min: 0, max: 100) @Logged var volume: Int
}

var config = AppConfig(profile: User(id: "u1", name: "Alice"), volume: 50)
print(config.$profile) // true
config.volume = 120 // "Setting 100"
print(config.volume) // 100

Released under the MIT License.