Swift Property Wrappers
A property wrapper is a type that adds a layer of logic between code and a stored value. The @propertyWrapper mark turns a type into such a wrapper.
Clamping a starting value
The Clamped wrapper keeps its value between a lower bound and an upper bound. The starting volume 150 sits above the upper bound, so the stored value becomes 100.
@propertyWrapper
struct Clamped {
private var value: Int
private let lower: Int
private let upper: Int
init(wrappedValue: Int, lower: Int, upper: Int) {
self.lower = lower
self.upper = upper
self.value = min(max(wrappedValue, lower), upper)
}
var wrappedValue: Int {
get {
return value
}
set {
value = min(max(newValue, lower), upper)
}
}
}
struct Settings {
@Clamped(lower: 0, upper: 100) var volume: Int = 150
}
let settings = Settings()
print(settings.volume)Clamping later assignments
Assigning 130 raises the incoming value above the upper bound, so the wrapper stores 100. Assigning a negative value drops it below the lower bound, so the wrapper stores 0.
@propertyWrapper
struct Clamped {
private var value: Int
private let lower: Int
private let upper: Int
init(wrappedValue: Int, lower: Int, upper: Int) {
self.lower = lower
self.upper = upper
self.value = min(max(wrappedValue, lower), upper)
}
var wrappedValue: Int {
get {
return value
}
set {
value = min(max(newValue, lower), upper)
}
}
}
struct Settings {
@Clamped(lower: 0, upper: 100) var volume: Int = 40
}
var settings = Settings()
print(settings.volume)
settings.volume = 130
print(settings.volume)
settings.volume = -5
print(settings.volume)Two properties, two ranges
The Game struct wraps two properties with the same wrapper type. Each property passes its own bounds, so lives uses the range 0 to 10 and level uses the range 1 to 5.
@propertyWrapper
struct Clamped {
private var value: Int
private let lower: Int
private let upper: Int
init(wrappedValue: Int, lower: Int, upper: Int) {
self.lower = lower
self.upper = upper
self.value = min(max(wrappedValue, lower), upper)
}
var wrappedValue: Int {
get {
return value
}
set {
value = min(max(newValue, lower), upper)
}
}
}
struct Game {
@Clamped(lower: 0, upper: 10) var lives: Int = 3
@Clamped(lower: 1, upper: 5) var level: Int = 9
}
let game = Game()
print(game.lives)
print(game.level)Values inside the range pass through
A value already inside the range needs no adjustment. The wrapper stores 21 and then 24 exactly as they were assigned.
@propertyWrapper
struct Clamped {
private var value: Int
private let lower: Int
private let upper: Int
init(wrappedValue: Int, lower: Int, upper: Int) {
self.lower = lower
self.upper = upper
self.value = min(max(wrappedValue, lower), upper)
}
var wrappedValue: Int {
get {
return value
}
set {
value = min(max(newValue, lower), upper)
}
}
}
struct Thermostat {
@Clamped(lower: 10, upper: 30) var temperature: Int = 21
}
var thermostat = Thermostat()
print(thermostat.temperature)
thermostat.temperature = 24
print(thermostat.temperature)Example
@propertyWrapper
struct Clamped {
private var value: Int
private let lower: Int
private let upper: Int
init(wrappedValue: Int, lower: Int, upper: Int) {
self.lower = lower
self.upper = upper
self.value = min(max(wrappedValue, lower), upper)
}
var wrappedValue: Int {
get {
return value
}
set {
value = min(max(newValue, lower), upper)
}
}
}
struct Player {
@Clamped(lower: 0, upper: 100) var health: Int = 80
}
var player = Player()
print(player.health)
player.health = 120
print(player.health)
player.health = -20
print(player.health)Key points
- The @propertyWrapper mark goes before the type that does the wrapping.
- A wrapper stores the real value and exposes it through a wrappedValue property.
- Every read of the wrapped property goes through the get part of wrappedValue.
- Every assignment to the wrapped property goes through the set part of wrappedValue.
- The set part can adjust the incoming value before storing it.
- A property uses a wrapper by writing the wrapper name as an attribute before it.
In this example, the Player struct wraps health with the Clamped wrapper using the bounds 0 and 100. The starting value 80 sits inside the range, so the first print shows 80. Assigning 120 stores the upper bound 100, and assigning a negative value stores the lower bound 0.
Note
One wrapper type can serve many properties, each with its own stored value.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
Which property exposes the stored value inside a property wrapper type?