๐ŸŽ“ A1Learner
๐Ÿ”
0 of 27 lessons completed

Swift Projected Values

A projected value is a second value that a property wrapper exposes beside its wrapped value. Reading it uses a dollar sign before the property name.

Reading a projected status

This Clamped wrapper also declares a projectedValue that reports whether the stored value was clamped. The starting value 50 was accepted unchanged, so the projected value reads accepted.

@propertyWrapper
struct Clamped {
    private var value: Int
    private let lower: Int
    private let upper: Int
    private var clamped: Bool
    init(wrappedValue: Int, lower: Int, upper: Int) {
        self.lower = lower
        self.upper = upper
        let fixed = min(max(wrappedValue, lower), upper)
        self.value = fixed
        self.clamped = fixed != wrappedValue
    }
    var wrappedValue: Int {
        get {
            return value
        }
        set {
            let fixed = min(max(newValue, lower), upper)
            clamped = fixed != newValue
            value = fixed
        }
    }
    var projectedValue: String {
        return clamped ? "clamped" : "accepted"
    }
}
struct Settings {
    @Clamped(lower: 0, upper: 100) var volume: Int = 50
}
let settings = Settings()
print(settings.volume)
print(settings.$volume)

A clamped starting value

The starting value 150 sits above the upper bound of 100. The wrapped value reads 100 and the projected value reports that clamping happened.

@propertyWrapper
struct Clamped {
    private var value: Int
    private let lower: Int
    private let upper: Int
    private var clamped: Bool
    init(wrappedValue: Int, lower: Int, upper: Int) {
        self.lower = lower
        self.upper = upper
        let fixed = min(max(wrappedValue, lower), upper)
        self.value = fixed
        self.clamped = fixed != wrappedValue
    }
    var wrappedValue: Int {
        get {
            return value
        }
        set {
            let fixed = min(max(newValue, lower), upper)
            clamped = fixed != newValue
            value = fixed
        }
    }
    var projectedValue: String {
        return clamped ? "clamped" : "accepted"
    }
}
struct Settings {
    @Clamped(lower: 0, upper: 100) var volume: Int = 150
}
let settings = Settings()
print(settings.volume)
print(settings.$volume)

The status follows each assignment

Each assignment updates the projected value along with the wrapped value. The overshoot reports clamped, and the later in-range assignment reports accepted again.

@propertyWrapper
struct Clamped {
    private var value: Int
    private let lower: Int
    private let upper: Int
    private var clamped: Bool
    init(wrappedValue: Int, lower: Int, upper: Int) {
        self.lower = lower
        self.upper = upper
        let fixed = min(max(wrappedValue, lower), upper)
        self.value = fixed
        self.clamped = fixed != wrappedValue
    }
    var wrappedValue: Int {
        get {
            return value
        }
        set {
            let fixed = min(max(newValue, lower), upper)
            clamped = fixed != newValue
            value = fixed
        }
    }
    var projectedValue: String {
        return clamped ? "clamped" : "accepted"
    }
}
struct Settings {
    @Clamped(lower: 0, upper: 100) var volume: Int = 40
}
var settings = Settings()
print(settings.$volume)
settings.volume = 130
print(settings.volume)
print(settings.$volume)
settings.volume = 70
print(settings.volume)
print(settings.$volume)

One projection per property

The Display struct wraps two properties, and each one projects its own status. The volume stayed in range while the brightness overshot, so their projected values differ.

@propertyWrapper
struct Clamped {
    private var value: Int
    private let lower: Int
    private let upper: Int
    private var clamped: Bool
    init(wrappedValue: Int, lower: Int, upper: Int) {
        self.lower = lower
        self.upper = upper
        let fixed = min(max(wrappedValue, lower), upper)
        self.value = fixed
        self.clamped = fixed != wrappedValue
    }
    var wrappedValue: Int {
        get {
            return value
        }
        set {
            let fixed = min(max(newValue, lower), upper)
            clamped = fixed != newValue
            value = fixed
        }
    }
    var projectedValue: String {
        return clamped ? "clamped" : "accepted"
    }
}
struct Display {
    @Clamped(lower: 0, upper: 100) var volume: Int = 60
    @Clamped(lower: 0, upper: 100) var brightness: Int = 140
}
let display = Display()
print(display.$volume)
print(display.$brightness)

Example

@propertyWrapper
struct Clamped {
    private var value: Int
    private let lower: Int
    private let upper: Int
    private var clamped: Bool
    init(wrappedValue: Int, lower: Int, upper: Int) {
        self.lower = lower
        self.upper = upper
        let fixed = min(max(wrappedValue, lower), upper)
        self.value = fixed
        self.clamped = fixed != wrappedValue
    }
    var wrappedValue: Int {
        get {
            return value
        }
        set {
            let fixed = min(max(newValue, lower), upper)
            clamped = fixed != newValue
            value = fixed
        }
    }
    var projectedValue: String {
        return clamped ? "clamped" : "accepted"
    }
}
struct Heater {
    @Clamped(lower: 10, upper: 30) var temperature: Int = 45
}
var heater = Heater()
print(heater.temperature)
print(heater.$temperature)
heater.temperature = 22
print(heater.temperature)
print(heater.$temperature)

Key points

  • A wrapper adds a projected value by declaring a projectedValue property.
  • Writing a dollar sign before the property name reads the projected value instead of the wrapped value.
  • The projected value can have a different type from the wrapped value.
  • The projected value often reports something about the wrapped value, such as whether it was adjusted.
  • Reading the plain property name still gives the wrapped value.
  • Each wrapped property has its own projected value.

In this example, the Heater struct wraps temperature with bounds 10 and 30, and the wrapper projects a status string. The starting value 45 is clamped to 30, so the projected value reads clamped. Assigning 22 stores the value unchanged, so the projected value then reads accepted.

Note

A wrapper without a projectedValue property offers no projected value to read.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

How is the projected value of a wrapped property named volume read?