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