Swift Property Wrappers for Reusable Logic

Explore the power of Swift Property Wrappers for encapsulating reusable logic, enhancing code readability, and reducing boilerplate in Swift development.

8.8 Property Wrappers for Reusable Logic

Intent

Property wrappers in Swift are a powerful feature designed to encapsulate common property logic, such as validation, synchronization, or transformation, thereby reducing boilerplate and improving code reuse. By using property wrappers, developers can abstract repetitive logic into reusable components, making code cleaner and more maintainable.

Implementing Property Wrappers in Swift

Property wrappers in Swift allow you to define a custom behavior for a property. They are defined using the @propertyWrapper attribute and can be applied to any property in your code.

Creating a Wrapper

To create a property wrapper, you start by defining a struct, class, or enum with the @propertyWrapper attribute. The core requirement is to implement a wrappedValue property, which represents the actual value being wrapped.

1@propertyWrapper
2struct Capitalized {
3    private var value: String = ""
4    
5    var wrappedValue: String {
6        get { value }
7        set { value = newValue.capitalized }
8    }
9}

In this example, the Capitalized property wrapper ensures that any string assigned to it is automatically capitalized.

Wrapped Value

The wrappedValue property is the centerpiece of a property wrapper. It defines how the value is stored and accessed. You can implement custom logic in the getter and setter to modify the behavior of the property.

 1@propertyWrapper
 2struct Clamped {
 3    private var value: Int
 4    private let range: ClosedRange<Int>
 5    
 6    var wrappedValue: Int {
 7        get { value }
 8        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
 9    }
10    
11    init(wrappedValue: Int, range: ClosedRange<Int>) {
12        self.range = range
13        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
14    }
15}

The Clamped property wrapper restricts an integer value to a specified range, ensuring it never exceeds the bounds.

Projecting Values

Sometimes, you might want to expose additional functionality or state beyond the wrappedValue. This is where projectedValue comes into play. By defining a projectedValue, you can provide extra capabilities to the users of your property wrapper.

 1@propertyWrapper
 2struct Logged<T> {
 3    private var value: T
 4    private(set) var projectedValue: [T] = []
 5    
 6    var wrappedValue: T {
 7        get { value }
 8        set {
 9            projectedValue.append(newValue)
10            value = newValue
11        }
12    }
13    
14    init(wrappedValue: T) {
15        self.value = wrappedValue
16    }
17}

In this Logged wrapper, every time the value changes, the change is logged in the projectedValue array.

Using Wrappers

To use a property wrapper, simply apply it to a property with the @ syntax. Here’s how you might use the Capitalized and Clamped wrappers:

1struct User {
2    @Capitalized var name: String
3    @Clamped(range: 0...100) var age: Int
4}
5
6var user = User(name: "john doe", age: 105)
7print(user.name) // Outputs: "John Doe"
8print(user.age)  // Outputs: 100

Use Cases and Examples

Property wrappers can be applied in a variety of scenarios to simplify and enhance your codebase.

Data Validation

One common use case for property wrappers is data validation. You can ensure that values meet certain criteria before they are accepted.

 1@propertyWrapper
 2struct ValidatedEmail {
 3    private var email: String = ""
 4    
 5    var wrappedValue: String {
 6        get { email }
 7        set {
 8            if isValidEmail(newValue) {
 9                email = newValue
10            } else {
11                print("Invalid email address")
12            }
13        }
14    }
15    
16    private func isValidEmail(_ email: String) -> Bool {
17        // Simple regex for demonstration purposes
18        let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
19        return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: email)
20    }
21}

This ValidatedEmail wrapper ensures that only valid email addresses are stored.

Lazy Initialization

Property wrappers can also be used to implement lazy initialization, delaying the creation of expensive resources until they are actually needed.

 1@propertyWrapper
 2struct Lazy<Value> {
 3    private var storage: (() -> Value)?
 4    private var value: Value?
 5    
 6    var wrappedValue: Value {
 7        mutating get {
 8            if let value = value {
 9                return value
10            } else if let storage = storage {
11                let value = storage()
12                self.value = value
13                self.storage = nil
14                return value
15            }
16            fatalError("Lazy value not initialized")
17        }
18    }
19    
20    init(initializer: @escaping () -> Value) {
21        self.storage = initializer
22    }
23}

With the Lazy wrapper, you can defer the initialization of a property until it is first accessed.

Thread Safety

For properties that require synchronized access in a multithreaded environment, property wrappers can provide a neat solution.

 1@propertyWrapper
 2class Synchronized<Value> {
 3    private var value: Value
 4    private let queue = DispatchQueue(label: "synchronized.queue")
 5    
 6    var wrappedValue: Value {
 7        get {
 8            return queue.sync { value }
 9        }
10        set {
11            queue.sync { value = newValue }
12        }
13    }
14    
15    init(wrappedValue: Value) {
16        self.value = wrappedValue
17    }
18}

The Synchronized wrapper ensures that access to the property is thread-safe.

Visualizing Property Wrappers

To better understand how property wrappers work, let’s visualize the flow of data using a simple diagram.

    graph TD;
	    A["Property Declaration"] --> B["Property Wrapper"]
	    B --> C["Wrapped Value Access"]
	    B --> D["Projected Value Access"]
	    C --> E["Getter/Setter Logic"]
	    D --> F["Additional Functionality"]

Diagram Description: This flowchart illustrates the process of using a property wrapper. The property declaration is linked to the property wrapper, which manages both the wrapped value and any projected values. The wrapped value is accessed through getter/setter logic, while the projected value provides additional functionality.

Knowledge Check

To reinforce your understanding of property wrappers, consider these questions:

  • How would you implement a property wrapper that automatically logs changes to a property?
  • What are the benefits of using property wrappers for data validation?
  • How can property wrappers improve thread safety in a Swift application?

Try It Yourself

Experiment with the provided examples by modifying the wrappers to add new features or constraints. For instance, try creating a property wrapper that limits a string’s length or one that encrypts and decrypts data on the fly.

Embrace the Journey

Remember, mastering property wrappers is just one step in your journey to becoming a proficient Swift developer. Keep exploring, experimenting, and learning new patterns to enhance your development skills.

Quiz Time!

Loading quiz…
Revised on Thursday, April 23, 2026