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

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?