9.
Property Wrappers
Written by Eli Ganim
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.
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, 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 already being used outside the Apple ecosystem on the Vapor project. Property wrappers, in this context, 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 for 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. 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 values. 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 wrappedValue, property wrappers may vend another type called projectedValue. You can use this to offer direct access to the unclamped value like this:
@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 you access with $ to get the unclamped, raw value.
Test it out with this:
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 have 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. It’s 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 shorthand that the compiler automatically expands, it’s clear that there’s nothing magical about them — but if you aren’t careful, thinking about them only in this way can tempt you to create unintuitive ones. To work with them day-to-day, you only need to focus on a few key terms: property wrapper, wrapped value and projected value.
The secret to these terms is not to take them literally because the names are misleading. To make their functions clearer, here’s a short set of working definitions:
- A property wrapper: Defines and presents a property via its
wrappedValue. - A wrapped value: Simply the value a property wrapper presents as
wrappedValue. - A projected value: An arbitrary value exposed by a property wrapper via
$syntax. It might not have any relationship with the wrapped value.
So how do these terms apply to the painting plan example?
-
@CopyOnWriteColorcreates aCopyOnWriteColorinstance. That instance is the property wrapper. - A client interacts with the instance via its
wrappedValueproperty. This is the wrapped value. -
CopyOnWriteColordoesn’t offer a projected value at all.
Note that the type of wrappedValue matches the type of the stored property bucketColor (Color). This property would exist even if you didn’t 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.
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. To illustrate projected values further, 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 at the property wrapper itself, not just its wrappedValue. projectedValue does just that.
You can make the projected value anything, including 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 o = Order()
// store a valid date string
o.orderPlacedDate = "2014-06-02"
o.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. A $ could mean anything.
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 that 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’s 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 such as
@MyWrapper var myproperty. It lets you define this logic so you can reuse it easily over many properties. -
A property wrapper’s
wrappedValuedefines the external interface to the value, which is exposed as the wrapped property itself, as inmyproperty. -
A property wrapper can have a projectedValue, which provides a handle for other interactions with the property wrapper. For example, it’s exposed via the $ syntax, as in
$myproperty. -
Property wrapping is conceptual. It doesn’t use the typical object-oriented programming 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.