26
Property Wrappers
Written by Alexis Gallagher
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
Back in Chapter 11, “Properties”, 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 run into property wrappers (and their telltale @-based, $-happy syntax) already. 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. They’re already being used outside the Apple ecosystem — for example, 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 that will show you some things to watch out for when using this language feature.
Basic example
To start with a simple use case for property wrappers, think back to the Color
type from the last chapter. 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 it’s much better to enlist the compiler’s help. 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
@propertyWrapper
says that 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
wrappedValue
vends 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 to go outside the zero-to-one bound.
Beginning in Swift 5.5, 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 you potentially lose the original value. To remedy this, you can use another feature of property wrappers. In addition to wrappedValue
, property wrappers 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 }
}
func printValueV2(@ZeroToOneV2 _ value: Double) {
print("The wrapped value is", value)
print("The projected value is", $value)
}
printValueV2(3.14)
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 }
}
func printValueV3(@ZeroTo(upper: 10) _ value: Double) {
print("The wrapped value is", value)
print("The projected value is", $value)
}
printValueV3(42)
Going generic
In the example, you used a Double
for the wrapped value. 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 }
}
Implementing CopyOnWrite
Now that you have the basic mechanics of property wrappers under your belt, it’s time to look at some more detailed examples.
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)
}
}
}
}
struct PaintingPlan {
@CopyOnWriteColor var bucketColor = .blue
}
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 }
}
@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)
}
}
}
}
struct PaintingPlan {
var accent = Color.white
@CopyOnWriteColor var bucketColor = .blue
@CopyOnWriteColor var bucketColorForDoor = .blue
@CopyOnWriteColor var bucketColorForWalls = .blue
// ...
}
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.
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.
struct Order {
@ValidatedDate var orderPlacedDate: String
@ValidatedDate var shippingDate: String
@ValidatedDate var deliveredDate: String
}
@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"
}
}
}
}
@propertyWrapper
public struct ValidatedDate {
// ... as above ...
public var projectedValue: DateFormatter {
get { formatter }
set { formatter = newValue }
}
}
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"
Challenges
Challenge 1: Create a generic property wrapper for CopyOnWrite
Consider the property wrapper CopyOnWriteColor
, which 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.
private class StorageBox<StoredValue> {
var value: StoredValue
init(_ value: StoredValue) {
self.value = value
}
}
Challenge 2: Implement @ValueSemantic
Using StorageBox
from the previous 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
.
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 with care. Here are some things to remember: