@testable Swift

A Swift blog by Anders Mannberg on test-driven development, functional programming and the stuff in between!

Reusable property wrappers

What are property wrappers?

Property wrappers is a feature that was added to Swift 5.1, which lets us decorate properties of different types with @-preceded keywords. Well-known examples are SwiftUI's @ObservableObject, @State and @Binding, just to name a few. We can easily define our own property wrappers with custom logic that can be triggered whenever our properties are assigned new values. The example below creates a property wrapper called @Lowercased. The wrappedValue property is required, and it's type - String in this case - determines which type can be decorated with the property wrapper.

@propertyWrapper
struct Lowercased {
    var wrappedValue: String {
        didSet { 
            wrappedValue = wrappedValue.lowercased()
        }
    }
    
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue.lowercased()
    }
}

We could then use this property wrapper as follows.

struct User {
    @Lowercased var firstName: String
    @Lowercased var familyName: String
}

let user = User(firstName: "Joe", familyName: "South")
print("Howdy, \(user.firstName) \(user.familyName)!")
//Howdy, joe south!

Pass me that function, please!

Although @Lowercased is a toy example, property wrappers can be very useful. Creating new ones isn't that much of a hassle, but if you're planning on using them heavily there are some things we can do to reduce the amount of boilerplate.

We could create a generic struct that takes the value we're wrapping together with a function that transforms it.

@propertyWrapper
struct Transformed<T> {
    private let transform: (T) -> T
    var wrappedValue: T {
        didSet { wrappedValue = transform(wrappedValue) }
    }
    
    init(wrappedValue: T, transform: @escaping (T) -> T) {
        self.transform = transform
        self.wrappedValue = transform(wrappedValue)
    }
}

We've now made it easy to define our wrapping logic on the fly.

struct User {
    @Transformed var firstName: String
    @Transformed var familyName: String
}

let user = User(
    firstName: Transformed(wrappedValue: "Joe") { $0.uppercased() }, 
    familyName: Transformed(wrappedValue: "South") { $0.lowercased() }
)

Rather than passing inline closures, we could easily wrap reusable functionality in an extension...

extension Transformed where T == String {
    static func uppercased(_ s: T) -> Transformed<T> {
        Transformed(wrappedValue: s, transform: { $0.uppercased() })
    }
    
    static func lowercased(_ s: T) -> Transformed<T> {
        Transformed(wrappedValue: s, transform: { $0.lowercased() })
    }
}

...which makes for a tidier call site.

let user = User(
    firstName: .uppercased("Joe"), 
    familyName: .lowercased("South")
)

Wrapper composition

There might be case when we'd like to add several property wrappers to a property, which is something the Swift compiler doesn't allow. For example, this won't compile

//⛔️ Multiple property wrappers are not supported
struct Dog {
    @Capitalized @Prefixed var name: String
}

To support chaining transformations, we could modify the Transformed struct to hold an array of functions which will be applied to the wrappedValue sequentially each time it's set.

@propertyWrapper
struct Transformed<T> {
    private let transforms: [(T) -> T]
    var wrappedValue: T {
        didSet {
            for f in transforms {
                wrappedValue = f(wrappedValue)
            }
        }
    }
    
    init(wrappedValue: T, _ transforms: ((T) -> T)...) {
        self.transforms = transforms
        self.wrappedValue = wrappedValue
        for f in transforms {
            self.wrappedValue = f(self.wrappedValue)
        }
    }
}

Now let's try it out!

import Foundation

struct Dog {
    @Transformed var name: String
    @Transformed var age: Int
}

var doggie = Dog(
    name: Transformed(
        wrappedValue: "Fido", 
        { $0.capitalized }, 
        { "\($0) the 🐶" }
    ),
    age: Transformed(
        wrappedValue: 2,
        { $0 * 7 }
    )
)

print(doggie.age)

doggie.name = "rolv"
doggie.age = 3

print("Our doggie has a new name, \(doggie.name), and is now \(doggie.age) dog years old")
Tagged with: