9.
Property Wrappers
Written by Alexis Gallagher
In the “Properties” chapter of Swift Apprentice: Fundamentals, you learned about property observers and how you can use them to affect the behavior of properties in a type. Property wrappers take that idea to the next level by letting you name and reuse the custom logic. They do this by moving the custom logic to an auxiliary type, which you may define yourself.
If you’ve worked with SwiftUI, you’ve already run into property wrappers (and their telltale @-based, $-happy syntax). SwiftUI uses them extensively because they allow virtually unlimited customization of property semantics, which SwiftUI needs in order to do its view update and data synchronization magic behind the scenes.
The Swift core team worked hard to make property wrappers a general-purpose language feature. For example, they’re used outside the Apple ecosystem on the Vapor project. In this context, property wrappers let you define a data model and map it to a database like PostgreSQL.
To learn the ins and outs of property wrappers, you’ll continue with some abstractions from the last chapter. You’ll begin with a simple example and then see an implementation of the copy-on-write pattern. Finally, you’ll wrap up with another example showing you some things to watch out for when using this language feature.
Basic Example
Consider the ‘Color’ type from the last chapter to start with a simple use case for property wrappers. It looked like this:
struct Color {
var red: Double
var green: Double
var blue: Double
}
There was an implicit assumption that the values red, green and blue fall between zero and one. You could have stated that requirement as a comment, but enlisting the compiler’s help is much better. To do that, create a property wrapper like this:
@propertyWrapper // 1
struct ZeroToOne { // 2
private var value: Double
private static func clamped(_ input: Double) -> Double { // 3
min(max(input, 0), 1)
}
init(wrappedValue: Double) {
value = Self.clamped(wrappedValue) // 4
}
var wrappedValue: Double { // 5
get { value }
set { value = Self.clamped(newValue) }
}
}
What’s so special here? Here’s what’s going on:
- The attribute
@propertyWrappersays this type can be used as a property wrapper. As such, it must vend a property calledwrappedValue. - In every other aspect, it’s just a standard type. In this case, it’s a struct with a private variable
value. - The private static
clamped(_:)helper method does a min/max dance to keep values between zero and one. - A wrapped value initializer is required for property wrapper types.
- The
wrappedValuevends the clamped value.
Now, you can use the property wrapper to add behavior to the color properties:
struct Color {
@ZeroToOne var red: Double
@ZeroToOne var green: Double
@ZeroToOne var blue: Double
}
That’s all it takes to guarantee the values are always locked between zero and one. The properties red, green, and blue are now wrapped properties. Try it out with this:
var superRed = Color(red: 2, green: 0, blue: 0)
print(superRed)
// r: 1, g: 0, b: 0
superRed.blue = -2
print(superRed)
// r: 1, g: 0, b: 0
No matter how hard you try, you can never get it outside the zero-to-one bounds.
You can use property wrappers with function arguments, too. Try this:
func printValue(@ZeroToOne _ value: Double) {
print("The wrapped value is", value)
}
printValue(3.14)
Here, the wrapped value printed is 1.0. @ZeroToOne adds clamping behavior to passed arguments. Pretty cool.
Projecting Values With $
In the above example, you clamp the wrapped value between zero and one but potentially lose the original value. To remedy this, you can use another feature of property wrappers. In addition to the wrappedValue, property wrappers may vend another property known as the projectedValue. You can use this to offer direct access to the unclamped value like so:
@propertyWrapper
struct ZeroToOneV2 {
private var value: Double
init(wrappedValue: Double) {
value = wrappedValue
}
var wrappedValue: Double {
get { min(max(value, 0), 1) }
set { value = newValue }
}
var projectedValue: Double { value }
}
In this version, the initializer and setter assign the value without clamping it. Instead, the wrappedValue getter does the clamping. This approach lets you use the projected value to get the initial, raw, unclamped value.
You access it with a $ prefix, like so:
func printValueV2(@ZeroToOneV2 _ value: Double) {
print("The wrapped value is", value)
print("The projected value is", $value)
}
printValueV2(3.14)
Not surprisingly, this prints out 1.0 for the wrapped value and 3.14 for the projected value. The wrapped value, value, and projected value, $value, are both Doubles in this example. However, as you’ll see later, this doesn’t have to be true.
Adding Parameters
The example clamps between zero and one, but you could imagine wanting to clamp between zero and 100 — or any other number greater than zero. You can do that with another parameter: upper. Try this definition:
@propertyWrapper
struct ZeroTo {
private var value: Double
let upper: Double
init(wrappedValue: Double, upper: Double) {
value = wrappedValue
self.upper = upper
}
var wrappedValue: Double {
get { min(max(value, 0), upper) }
set { value = newValue }
}
var projectedValue: Double { value }
}
This version adds an upper bound that you must specify. Try it out in the playground:
func printValueV3(@ZeroTo(upper: 10) _ value: Double) {
print("The wrapped value is", value)
print("The projected value is", $value)
}
printValueV3(42)
To specify the upper parameter, write the property wrapper like this: @ZeroTo(upper: 10). The example above will print 10 for the wrapped value and 42 as the projected value, respectively.
Going Generic
You used a Double for the wrapped value in the previous example. The property wrapper can also be generic with respect to the wrapped value. Try this:
@propertyWrapper
struct ZeroTo<Value: Numeric & Comparable> {
private var value: Value
let upper: Value
init(wrappedValue: Value, upper: Value) {
value = wrappedValue
self.upper = upper
}
var wrappedValue: Value {
get { min(max(value, 0), upper) }
set { value = newValue }
}
var projectedValue: Value { value }
}
Instead of Double, this version uses the generic placeholder Value everywhere. You can use it like before, except this time, you can use it with Double, Float, Float16, Int and so on. The compiler infers the Value type from the wrapped type you use, and this type only needs to fulfill the requirement that it’s Numeric and Comparable.
Implementing Copy-on-Write
Now that you understand the basic mechanics of property wrappers, it’s time to look at more detailed examples.
As the discussion from the last chapter shows, the copy-on-write pattern is verbose. You must define the private, stored reference-type property for the backing storage (the bucket) and the visible, computed property that preserves value semantics (the bucketColor). Then, in the getter and setter, you also need to define the copy-on-write logic.
As you might have guessed, this is also an example of a pattern you can simplify using property wrappers.
Recall that, in the previous chapter, you defined PaintingPlan with a computed property bucketColor:
struct PaintingPlan { // a value type, containing ...
// ...
// a computed property facade over deep storage
// with copy-on-write and in-place mutation when possible
var bucketColor: Color {
get {
bucket.color
}
set {
if isKnownUniquelyReferenced(&bucket) {
bucket.color = bucketColor
} else {
bucket = Bucket(color: newValue)
}
}
}
}
With a CopyOnWriteColor property wrapper, you can replace the above code with this simpler code:
struct PaintingPlan {
@CopyOnWriteColor var bucketColor = .blue
}
As before, this handy syntax lets you create dozens of copy-on-write properties. But how does it work?
Compiler Expansion
The compiler automatically expands @CopyOnWriteColor var bucketColor = .blue into the following:
private var _bucketColor = CopyOnWriteColor(wrappedValue: .blue)
var bucketColor: Color {
get { _bucketColor.wrappedValue }
set { _bucketColor.wrappedValue = newValue }
}
This substitution reproduces parts of the original version of your code, including the internal computed property bucketColor and the private storage property _bucketColor.
But where did all the tricky logic go? It now lives in a dedicated custom property wrapper type, CopyOnWriteColor, which enables the custom @CopyOnWriteColor. CopyOnWriteColor has the same type as the private _bucketColor, which serves as the actual underlying stored property.
Here’s the definition of CopyOnWriteColor:
@propertyWrapper
struct CopyOnWriteColor {
private var bucket: Bucket
init(wrappedValue: Color) {
self.bucket = Bucket(color: wrappedValue)
}
var wrappedValue: Color {
get {
bucket.color
}
set {
if isKnownUniquelyReferenced(&bucket) {
bucket.color = newValue
} else {
bucket = Bucket(color:newValue)
}
}
}
}
In PaintingPlan, assigning an initial value of .blue to bucketColor initializes an instance of the property wrapper CopyOnWriteColor, which defines its own bucket.
Then, when you read or write bucketColor, you call the getters and setters of the computed property wrappedValue in CopyOnWriteColor. These getters and setters implement the same copy-on-write logic as your original implementation.
It’s a bit opaque because of the two levels of delegation: first through the property wrapper and then through its computed property. But, at its core, this is just plain old code reuse. You write the tricky copy-on-write logic once, then refer to it when using the custom attribute. So this makes it easy to write a more elaborate painting plan:
struct PaintingPlan {
var accent = Color.white
@CopyOnWriteColor var bucketColor = .blue
@CopyOnWriteColor var bucketColorForDoor = .blue
@CopyOnWriteColor var bucketColorForWalls = .blue
// ...
}
As you saw earlier, property wrappers can be generic, making them even more reusable. You’ll explore generic property wrappers again for copy-on-write in a Challenge later in the chapter.
Wrappers, Projections and Other Confusables
When you think about property wrappers as a shorthand that the compiler automatically expands, it’s clear that there’s nothing magical about them. But nevertheless, the terminology can be confusing.
To work with them day-to-day, it helps to be precise about language and to focus on a few key terms: property wrapper, wrapped property, wrapped value,and projected value. Here’s a short set of working definitions:
- A property wrapper: The type which defines and presents a property via its
wrappedValue. - A wrapped property. The property to which the wrapper is applied, and whose name will be used to to access the
wrappedValue. - A wrapped value: The value a property wrapper presents from its own
wrappedValueproperty, which is accessed via the wrapped property. - A projected value: Another value which a property wrapper might define. If defined, it is exposed by a property wrapper via
$syntax. It need not relate to the wrapped value.
So how do these terms apply to the painting plan example?
- Each
@CopyOnWriteColorcreates aCopyOnWriteColorinstance. That instance is the property wrapper. - A client interacts with an instance’s
wrappedValueproperty, when it calls one of the wrapped properties, likebucketColor,bucketColorForDoor, orbucketColorForWalls. -
CopyOnWriteColordoesn’t offer a projected value at all.
Note that the type of wrappedValue matches the type of the stored property bucketColor (Color) which would exist if you did not apply the wrapper. However, once you apply the property, it is not the case that the original stored property still exists “underneath the wrapper” in any sense. In other words, the wrapping is purely conceptual, not physical, and the word “wrapper” is misleading.
Projected Values are Handles
A projected value is nothing more than an additional handle that a property wrapper can offer. As you saw earlier, it’s defined by projectedValue and exposed as $name, where “name” is the name of the wrapped property.
Projected values don’t need to be the same type as the wrapped value. Nor do they need to be the “true” type of some underlying value which is “projected” through the “wrapping” to become the wrapped value. They can be anything. To illustrate this, you’ll create a new example that uses property wrappers to transform values.
Suppose you read in a text file formatted as comma-separated values (CSV). Every row contains key dates about a product order, such as when the order was placed, shipped and delivered. You load these dates into a struct.
You also want to validate that the dates are written in your preferred date format: yyyy-mm-dd. For instance, you’d write Swift’s birthday, June 2, 2014, as "2014-06-02".
You could enforce this validation by applying a @ValidatedDate annotation as follows:
struct Order {
@ValidatedDate var orderPlacedDate: String
@ValidatedDate var shippingDate: String
@ValidatedDate var deliveredDate: String
}
To achieve this, you define this property wrapper:
@propertyWrapper
public struct ValidatedDate {
private var storage: Date? = nil
private(set) var formatter = DateFormatter()
public init(wrappedValue: String) {
self.formatter.dateFormat = "yyyy-mm-dd"
self.wrappedValue = wrappedValue
}
public var wrappedValue: String {
set {
self.storage = formatter.date(from: newValue)
}
get {
if let date = self.storage {
return formatter.string(from: date)
} else {
return "invalid"
}
}
}
}
Note: The
DateandDateFormattertypes are part of theFoundationlibrary. If you’ve been coding along in a playground and are now getting compiler errors, addimport Foundationto your playground before the code above. Common practice is putting allimportstatements at the beginning of a file.
The property wrapper encapsulates the conversion logic. Whenever you store a date string like "2014-06-02" in orderPlacedDate, you convert that string and store it as a Date in the wrapper’s storage. Whenever you read the property, you convert it back to a string. If you try to store an invalid string, wrappedValue will return "invalid".
But what if, for instance, you wanted to change the date format you’re using? You need a way to get inside the property wrapper itself, not just at its wrappedValue. projectedValue lets you do that.
Since you can make the projected value anything, you can even make it the date formatter object:
@propertyWrapper
public struct ValidatedDate {
// ... as above ...
public var projectedValue: DateFormatter {
get { formatter }
set { formatter = newValue }
}
}
Updating the wrapper’s projectedValue updates the underlying DateFormatter to use a new date format.
You access the projected value with a $. Just as a reference to the wrapped property orderPlaceDate really accesses the wrapper’s wrappedValue, a reference to $orderPlacedDate really accesses the wrapper’s projectedValue.
This example shows the syntax in action:
var order = Order()
// store a valid date string
order.orderPlacedDate = "2014-06-02"
order.orderPlacedDate // => 2014-06-02
// update the date format using the projected value
let otherFormatter = DateFormatter()
otherFormatter.dateFormat = "mm/dd/yyyy"
order.$orderPlacedDate = otherFormatter
// read the string in the new format
order.orderPlacedDate // => "06/02/2014"
As this example shows, you can use a property wrapper’s projected value for anything. The lesson here is that you must study the property wrapper’s documentation to understand the meaning of $name in any particular case.
Challenges
Challenge 1: Create a Generic Property Wrapper for CopyOnWrite
Consider the property wrapper CopyOnWriteColor you defined earlier in this chapter. It lets you wrap any variable of type Color. It manages the sharing of an underlying storage type, Bucket, which owns a single Color instance. Thanks to structural sharing, multiple CopyOnWriteColor instances might share the same Bucket instance — thus sharing its Color instance and saving memory.
To implement the copy-on-write logic, what matters about Bucket is not its domain semantics, like isRefilled, but just that it’s a reference type. You only used it as a box for Color.
Since property wrappers can be generic, try defining a generic copy-on-write property wrapper type, CopyOnWrite. Instead of being able to wrap only Color values, it should be generic over any value semantic type which it wraps. Instead of using a dedicated storage type like Bucket, it should provide its own box type for storage.
Your challenge: Write the definition for this generic type, CopyOnWrite, and use it in an example to verify that the wrapped properties preserve the value semantics of the original type.
To get you started, here’s a suitable definition of a box type:
private class StorageBox<StoredValue> {
var value: StoredValue
init(_ value: StoredValue) {
self.value = value
}
}
Challenge 2: Implement @ValueSemantic
Using StorageBox from the last challenge and the following protocol, DeepCopyable, as a constraint, write the definition for a generic property wrapper @ValueSemantic. Then use it in an example to verify that wrapped properties have value semantics even when wrapping an underlying type that doesn’t. Example: NSMutableString is an example of a non-value semantic type. Make it conform to DeepCopyable and test it with @ValueSemantic.
Hints:
-
If the
DeepCopyableconforming type is a reference type or otherwise doesn’t have value semantics, making a deep copy ensures properties don’t share any storage and changes to one don’t affect the other. -
Note that if the conforming type already has value semantics, it meets these requirements, so it’s enough to return
self. In this case, however, there’s no point in using@ValueSemantic.
protocol DeepCopyable {
/* Returns a deep copy of the current instance.
If `x` is a deep copy of `y`, then:
- The instance `x` should have the same value as `y`
(for some sensible definition of value – not just
memory location or pointer equality!)
- It should be impossible to do any operation on `x`
that will modify the value of the instance `y`.
Note: A value semantic type implementing this protocol can just
return `self` since that fulfills the above requirement.
*/
func deepCopy() -> Self
}
Key Points
Property wrappers have a lot of flexibility and power, but you also need to use them carefully. Here are some things to remember:
-
Unusual SwiftUI syntax that uses
@and$characters is not unique to SwiftUI. It is often an advanced application of property wrappers, a language feature anyone can use. -
A property wrapper lets you apply custom logic to define the behavior of reading and writing a property with syntax like
@MyWrapper var myproperty. It lets you define this logic so you can reuse it easily over many properties. -
A property wrapper’s wrappedValue defines the external interface to the value, but it is accessed as the wrapped property itself, as in
myproperty. -
A property wrapper can also have a projectedValue which provides a handle for other interactions with the property wrapper. It is exposed via the $ syntax, as in
$myproperty. -
Property wrapping is conceptual. It does not necessarily represent the typical object-oriented design pattern where one object acts as an adapter by physically wrapping another actual object. Consequently, there isn’t necessarily a stored property or value that exists untouched “underneath” the wrapper.