Swift Property Wrappers
Introduction
Property wrappers are a powerful feature introduced in Swift 5.1 that allow you to extract common property-related patterns into reusable code. They act as a layer between the code that defines a property and the code that uses it, allowing you to add behavior or validation without cluttering your property definitions.
Think of a property wrapper as a special container around your property that can run additional code each time you get or set its value. This feature enables clean, reusable code by separating the concerns of property storage from the logic applied to it.
Understanding Property Wrappers
The Basics
A property wrapper is defined as a structure, enumeration, or class that contains a wrappedValue
property. You apply a property wrapper by writing its name with the @
prefix before your property declaration.
Let's start with a simple example:
@propertyWrapper
struct Trimmed {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.trimmings(characterSet: .whitespacesAndNewlines) }
}
init(wrappedValue initialValue: String) {
self.wrappedValue = initialValue
}
}
With this property wrapper defined, we can now use it on string properties:
struct User {
@Trimmed var name: String
@Trimmed var email: String
}
// Usage
var user = User(name: " John Doe ", email: " [email protected] ")
print("Name: '\(user.name)'") // Output: Name: 'John Doe'
print("Email: '\(user.email)'") // Output: Email: '[email protected]'
When we assign a string with extra whitespace to either property, the wrapper automatically trims it, giving us clean values without requiring additional code at each assignment point.
How Property Wrappers Work
Under the hood, Swift creates an instance of your wrapper type and uses it to manage the property. The @Trimmed
annotation in our example above is roughly equivalent to:
struct User {
private var _name = Trimmed(wrappedValue: "")
var name: String {
get { _name.wrappedValue }
set { _name.wrappedValue = newValue }
}
private var _email = Trimmed(wrappedValue: "")
var email: String {
get { _email.wrappedValue }
set { _email.wrappedValue = newValue }
}
}
This transformation is handled automatically by the Swift compiler, making our code cleaner and more focused.
Creating Custom Property Wrappers
Now that we understand the basics, let's create more useful property wrappers for common tasks.
Example 1: Clamping Values
Let's create a property wrapper that ensures a numeric value stays within a defined range:
@propertyWrapper
struct Clamping<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)
}
}
Usage:
struct Temperature {
@Clamping(range: 0...100) var celsius: Double
}
var temp = Temperature(celsius: 37.0)
temp.celsius = -10 // Will be clamped to 0
print(temp.celsius) // Output: 0
temp.celsius = 120 // Will be clamped to 100
print(temp.celsius) // Output: 100
Example 2: User Defaults Storage
Property wrappers are perfect for abstracting away persistence logic:
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
Usage:
class SettingsManager {
@UserDefault(key: "isDarkModeEnabled", defaultValue: false)
var isDarkModeEnabled: Bool
@UserDefault(key: "username", defaultValue: "Guest")
var username: String
@UserDefault(key: "refreshInterval", defaultValue: 60)
var refreshInterval: Int
}
let settings = SettingsManager()
print(settings.username) // Output: "Guest" (or previously stored value)
settings.username = "JohnDoe"
// Now "JohnDoe" is automatically saved to UserDefaults
Advanced Property Wrapper Features
Accessing the Wrapper Instance
Sometimes you need to access the wrapper itself, not just the wrapped value. Swift provides a special $
prefix syntax for this:
@propertyWrapper
struct Capitalized {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.capitalized }
}
var projectedValue: String {
return value.uppercased()
}
}
struct Book {
@Capitalized var title: String
}
var book = Book(title: "the swift programming language")
print(book.title) // Output: "The Swift Programming Language" (capitalized)
print(book.$title) // Output: "THE SWIFT PROGRAMMING LANGUAGE" (all uppercase)
The projectedValue
is what gets returned when you use the $
prefix.
Wrapper Composition
One powerful aspect of property wrappers is that they can be composed:
struct User {
@Trimmed @Capitalized var name: String
}
var user = User(name: " john doe ")
print(user.name) // Output: "John Doe"
In this case, Trimmed
is applied first, then Capitalized
.
Real-World Applications
Form Validation
Property wrappers are perfect for form validation:
@propertyWrapper
struct EmailValidated {
private var email: String = ""
private(set) var isValid: Bool = false
var wrappedValue: String {
get { email }
set {
email = newValue
isValid = isValidEmail(email)
}
}
var projectedValue: Bool {
return isValid
}
private func isValidEmail(_ email: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
}
class RegistrationForm {
@EmailValidated var email: String = ""
func canSubmit() -> Bool {
return $email // Accessing the isValid property
}
}
let form = RegistrationForm()
form.email = "not-valid-email"
print(form.canSubmit()) // Output: false
form.email = "[email protected]"
print(form.canSubmit()) // Output: true
Thread-Safe Properties
Property wrappers can help with concurrency:
@propertyWrapper
struct ThreadSafe<Value> {
private var value: Value
private let lock = NSLock()
var wrappedValue: Value {
get {
lock.lock()
defer { lock.unlock() }
return value
}
set {
lock.lock()
defer { lock.unlock() }
value = newValue
}
}
init(wrappedValue: Value) {
self.value = wrappedValue
}
}
class DataManager {
@ThreadSafe var sharedData: [String] = []
func add(_ item: String) {
sharedData.append(item)
}
}
This ensures that access to sharedData
is synchronized across different threads.
SwiftUI Integration
SwiftUI leverages property wrappers extensively with @State
, @Binding
, @ObservedObject
, etc. Here's how you might implement a simplified version of @State
:
@propertyWrapper
struct SimpleState<Value> {
private var value: Value
private var onChange: (Value) -> Void
var wrappedValue: Value {
get { value }
set {
value = newValue
onChange(newValue)
}
}
init(wrappedValue: Value, onChange: @escaping (Value) -> Void) {
self.value = wrappedValue
self.onChange = onChange
}
}
// Usage would be:
class MyView {
@SimpleState(onChange: { newValue in
print("Value changed to: \(newValue)")
// In SwiftUI, this would trigger a view refresh
})
var counter: Int = 0
}
Summary
Property wrappers are a powerful Swift feature that allows you to extract common property-related logic into reusable components. They help you:
- Reduce boilerplate by abstracting away common patterns
- Enforce validation without cluttering your model code
- Separate concerns between property storage and the logic applied to it
- Improve readability by clearly expressing your property's behavior
By mastering property wrappers, you can write cleaner, more expressive, and more maintainable Swift code.
Further Resources
- Swift Documentation on Property Wrappers
- WWDC 2019: Modern Swift API Design
- Swift Evolution Proposal: SE-0258
Exercises
- Create a
@Debounced
property wrapper that only updates its value after a specified time interval has passed without new changes. - Implement a
@Validated
property wrapper that takes a validation function as a parameter. - Design a
@Logged
property wrapper that prints changes to a property. - Create a property wrapper that automatically converts between different units of measurement (e.g., miles to kilometers).
- Try combining multiple property wrappers and observe their interaction behaviors.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)