Swift Property Wrappers

Episode #423 | 20 minutes | published on January 10, 2020 | Uses Xcode-11.2.1, Swift-5.1
Subscribers Only
Swift 5.1 introduced a powerful new feature to the language: Property Wrappers. In this video we will take a look at how to write property wrappers and how to use them to simplify code in our apps.

Episode Links

Swift 5.1 introduced a powerful new feature to the language: Property Wrappers. In this video we will take a look at how to write property wrappers and how to use them to simplify code in our apps.

Let’s start with a simple struct to represent a student:

struct Student {
    var grade: Double
} 

Here we have a student struct that has a grade. If we want to add behavior to the grade property, we can use a property wrapper.

The simplest one could just log when the getter or setter are called.

@propertyWrapper
struct PrintAccess<Value> {

    private var value: Value

    init(wrappedValue value: Value) {
        self.value = value
    }

    var wrappedValue: Value {
        get {
            print("Getting the value...")
            return value
        }
        set {
            print("Setting value to \(newValue)")
            value = newValue
        }
    }
}

Now we can mark the grade property in the Student struct with this new property wrapper:

    @PrintAccess var grade: Double = 0

With this in place we can create a student, get its grade property, and then set it to a new value and inspect the console for the results:

var student = Student(grade: 86)
student.grade
student.grade = 90

Getting the value…
Setting value to 90.0

Notice that we didn’t get notified with the initial value of 86. This is how property wrappers work, and it’s something you should be aware of.

Why go through this trouble? Why not just add a get/set handler?

One of the reasons you would do this is to reuse this functionality across multiple properties. So we can add a new property for the number of credits the student has:

    @PrintAccess var credits: Int = 0

And now we can get a log entry when either of these are changed:

student.credits += 1

Another property wrapper I have found useful is one that trims leading & trailing whitespace from values that were entered by the user. This can often cause frustration when doing equality checking & validation.

Let’s create a property wrapper to address this.

@propertyWrapper
struct Trimmed {
    var value: String

    init(wrappedValue value: String) {
        self.value = value
        self.wrappedValue = value
    }

    var wrappedValue: String {
        get {
            value
        }
        set {
            value = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
        }
    }
}

Note that in order to get a trimmed value from the initializer we have to pass the value through the wrappedValue property setter.

Now if we had a struct like this:

struct User {
    @Trimmed
    var email: String
}

We could automatically trim the input to have no leading or trailing whitespace:

var user = User(email: " hi@example.com  ")
print("[\(user.email)]")

We can also pass arguments to property wrappers. Let’s say we want to constrain the range of values for a given property.

@propertyWrapper
struct Clamped<Value : Comparable> {
    @PrintAccess
    private var value: Value
    private var range: ClosedRange<Value>

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        value = wrappedValue
        self.range = range
    }

    var wrappedValue: Value {
        get { value }
        set {
            value = min(max(range.lowerBound, newValue), range.upperBound)
        }
    }

}
struct Player {
    @Clamped(0...100)
    var speed: Double = 0
}

var player = Player()
player.speed = 50
player.speed = 150

Note that we’ve also decorated the value as @PrintAccess so we can see when it gets changed!

What else can we do? Let’s say we wanted to have an audit log of changes to a property over time:


@propertyWrapper
struct AuditLog<Value> {
    private var value: Value
    private(set) var changes: [(Date, Value)] = []
    private let maxHistory = 10

    init(wrappedValue value: Value) {
        self.value = value
    }

    var wrappedValue: Value {
        get { value }
        set {
            value = newValue
            changes.append((Date(), newValue))
            if changes.count > maxHistory {
                changes.removeFirst()
            }
        }
    }
}

And a sample Account type that uses it:

struct Account {
    @AuditLog
    private(set) var balance: Double = 100

    mutating func deposit(_ amount: Double) {
        balance += amount
    }

    mutating func withdraw(_ amount: Double) {
        balance -= amount
    }

    func audit() {
        // ??
    }
}

How would we get at this changes array to see the data?

There are 2 ways to do this. The first, is by leveraging the underscore operator to get at the property wrapper instance itself:

func audit() {
    print(_balance.changes)
}

This only works inside the type. If we want to expose this behavior (or any behavior really) outside of our instance, we’ll have to use what is called a projected value.

A projected value is a property on a property wrapper type that you can implement to expose whatever data you want. Often times you’ll just return the property wrapper itself:

    var projectedValue: Self {
        return self
    }

Now we can access this from outside the account instance using the dollar sign operator:

print(account.$balance.changes)

What are some of the limitations?

  • No error handling
  • Difficult to compose

This is a good start on how to use the new property wrappers in Swift. It enables a whole new style of API design, and while it does come with some limitations, I’m excited to see the interesting stuff the community has built around them.

blog comments powered by Disqus