Property Wrappers

Introduction

Manipulating data as it is stored into or retrieved from a property is a common task in programming.

You could implement a computed property coupled with private stored property, where the get and set blocks of the computed property change the stored property’s data. That might look like this:

    struct Car {
        private var fuelLevelStorage: Double = 1.0
        var fuelLevel: Double {
            set {
                fuelLevelStorage = max(min(newValue, 1), 0)
            }
            get {
                return fuelLevelStorage
            }
        }
    }

Here, the internal fuelLevel computed property allows users of the Car type to set and retrieve a value that is secretly stored in the fuelLevelStorage private variable. The set block clamps to the range of 0.0 through 1.0 by storing a value of 1.0 when the value is too high or storing a value of 0.0 when the value is too low.

Unfortunately, this approach clutters the defining type. It also is not reusable. What if you need to store the percentage completion of a task? What if you need to store the percentage brightness of a light bulb? Despite the formulaic nature of the code above, you would need to repeat it everywhere you need to clamp a value between 0.0 and 1.0.

In this tutorial, you will learn to use a property wrapper to define a reusable abstraction for manipulating data coming into and out of your properties. A property wrapper is a specialized enum, struct, or class that you define to hold and manipulate a wrapped value that uses computed properties internally. You can wrap a stored property of any other type in an instance of your wrapper type – and your code becomes much less cluttered and more readable as a result. To understand this process, get started with an example.

Accessing the Wrapper Itself

A type with a wrapped property, such as Car and its fuelLevel, can access the wrapper object directly – rather than its wrappedValue – by prefixing the wrapped property name with an underscore ( _ ). Demonstrate this feature by making Car conform to CustomStringConvertible and logging the wrapper from the description property:

Listing 26.8  Printing the wrapper to the console (PercentageClamping)

struct Car {
    @Percentage var fuelLevel: Double = 1.0
    @Percentage var wiperFluidLevel: Double = 0.5
    @Percentage(upperBound: 2.0) var stereoVolume: Double = 1.0
}

extension Car: CustomStringConvertible {
    var description: String {
        return "fuelLevel: \(fuelLevel), wrapped by \(_fuelLevel)"
    }
}

var myCar = Car()
print(myCar)
myCar.fuelLevel = 1.1
print("Fuel:", myCar.fuelLevel)
...

The first line of your output should show the truth of your wrapper:

    fuelLevel: 1.0, wrapped by Percentage(storage: 1.0, upperBound: 1.0)

Inside the implementation of Car – and even in an extension on Car in the same file – you can access the Percentage instance that is wrapping the fuelLevel property via its underscore-prefixed version, _fuelLevel. But note that it is private to the Car type. For example, you would not be able to print myCar._fuelLevel from outside the struct or its extension (try it).

Projecting Related Values

While debugging a program that uses your Percentage property wrapper, you might want to find out, at any given point, what the last assignment to a variable’s value was – before it was clamped into the allowed range.

Change the behavior of the initializer and wrapped value to store any value and to do its clamping work when the property is read, instead of written.

Listing 26.9  Clamping when reading instead of writing (Percentage.swift)

@propertyWrapper public struct Percentage {

    private var storage: Double
    private var upperBound: Double

    public init(wrappedValue: Double, upperBound: Double = 1) {
        storage = max(min(wrappedValue, 1), 0)
        storage = wrappedValue
        self.upperBound = upperBound
    }

    public var wrappedValue: Double {
        set {
            storage = max(min(newValue, upperBound), 0)
            storage = newValue
        }
        get {
            return storage
            return max(min(storage, upperBound), 0)
        }
    }
}

Now an instance of Percentage stores the last value assigned to its wrappedValue, regardless of magnitude. At any given time, the storage contains a value that may or may not be within range, but reading the wrappedValue will produce the clamped value. To the call site in the main playground content, nothing has changed. Storing a too-large value and then reading it will still report the clamped value.

Now, to give users of your property wrapper access to the un-clamped value, your property wrapper can project an additional value related to its wrapped value. You do this by implementing a projectedValue property:

Listing 26.10  Projecting a value from a wrapper (Percentage.swift)

@propertyWrapper public struct Percentage {
    ...
    public var projectedValue: Double {
        get {
            return storage
        }
    }
}

Here, you project the value of storage without clamping it. That value can be accessed by prefixing a wrapped variable’s name with $. Test your ability to access this projected value in the main playground:

Listing 26.11  Accessing the projected value (PercentageClamping)

...
var myCar = Car()
print(myCar)
myCar.fuelLevel = 1.1
print("Fuel:", myCar.fuelLevel)
myCar.stereoVolume = 2.5
print("Volume:", myCar.stereoVolume)
print("Projected volume:", myCar.$stereoVolume)

When you access a wrapped property on an object, prefixing the property name with $ gives you access to the projectedValue instead of the wrappedValue. Your final playground output should be:

    fuelLevel: 1.0, wrapped by Percentage(storage: 1.0, upperBound: 1.0)
    Fuel: 1.0
    Volume: 2.0
    Projected volume: 2.5

Note that a property wrapper’s projectedValue can have both a getter and a setter and can represent any value related to the wrappedValue. It does not even have to be the same type. For example, you could have implemented projectedValue to return a Bool indicating whether the value in storage is within the allowed range.

It is up to the implementer of a property wrapper to decide whether to project an additional value and what that value might be, so check the documentation of any property wrapper you adopt in your own programs to understand whether you will find its projected value useful.

Property wrappers are a flexible way to define custom behaviors that should be executed when a property is accessed, and they make it easy to define those behaviors in an abstract and reusable way.

Written by

XR Developer responsible for end-to-end development of XR solutions spanning multiple domains, by using various XR and WebXR libraries.

Leave a Reply