
This video is only available to subscribers. Start a subscription today to get access to this and 484 other videos.
Swift Property Wrappers
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.