How it works
The explanation, with examples you can run.
Writing one
@propertyWrapperstruct Clamped<Value: Comparable> { private var value: Value let range: ClosedRange<Value> init(wrappedValue: Value, _ range: ClosedRange<Value>) { self.range = range self.value = min(max(wrappedValue, range.lowerBound), range.upperBound) } var wrappedValue: Value { get { value } set { value = min(max(newValue, range.lowerBound), range.upperBound) } } var projectedValue: Clamped<Value> { self } // exposed as $name} struct Review { @Clamped(1...5) var stars = 3} var r = Review()r.stars = 9print(r.stars) // 5print(r.$stars.range) // 1...5The compiler expands @Clamped(1...5) var stars = 3 to roughly:
private var _stars = Clamped(wrappedValue: 3, 1...5)var stars: Int { get { _stars.wrappedValue } set { _stars.wrappedValue = newValue }}var $stars: Clamped<Int> { _stars.projectedValue }wrappedValueis required. The initial value after=is passed asinit(wrappedValue:).projectedValueis optional, and is what$starsreturns. It can be any type._starsis the wrapper instance, private to the enclosing type.
Wrappers also work on local variables and function parameters (Swift 5.5), not only properties.
How SwiftUI uses them
SwiftUI views are structs that are recreated constantly, so they can't hold state in ordinary stored properties. @State is a property wrapper whose storage lives outside the struct, in SwiftUI's graph, keyed by the view's identity. The wrapper in the struct is a handle to it, and reading wrappedValue during body is how SwiftUI learns that the view depends on it.
@State var isOn = false:isOnis theBool;$isOnis the projectedBinding<Bool>.@Binding var isOn: Bool: a wrapper around aBindingpassed in from a parent.@Environment(\.colorScheme) var scheme: reads a value from the environment.@AppStorage("key"): reads and writesUserDefaults.
Where wrappers stop and macros start
A wrapper sees only its own property. It can't add members to the enclosing type or look at other properties, and a wrapper on a class property can't easily tell the class that something changed. @Published worked around that with a special hook into ObservableObject. Observation (@Observable, SE-0395) took the other route: it is an attached macro that rewrites every stored property of a class and adds the tracking code, which a property wrapper couldn't do. That's the subject of the macros topic.
Property wrappers can't be combined with lazy, weak or unowned, but willSet and didSet still work on a wrapped property. Stacking several (@A @B var x) is allowed: the first one listed is the outermost wrapper.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.