@propertyWrapper in Swift: beyond UserDefaults
What property wrappers are, how to build one from scratch, and when to use them — with practical examples: @Clamped, @Trimmed, and projectedValue.
If you’ve already read the feature flags post, you’ve already used a property wrapper. But @propertyWrapper can do a lot more than wrap UserDefaults. It’s one of those Swift features that, once you understand it, you start seeing it everywhere.
What it is
A property wrapper is a struct that controls how a property’s value is read and written. The compiler generates the access code for you — you define the rules once and reuse them wherever you want.
When I first saw it I thought there was something special going on in the compiler. There isn’t. It’s syntactic sugar over a pattern you could write yourself — what it saves you is having to repeat that logic on every property.
The anatomy
Every property wrapper needs exactly one thing: wrappedValue.
@propertyWrapper
struct Uppercased {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.uppercased() }
}
}
When you write @Uppercased var name: String, the compiler expands it to:
private var _name = Uppercased()
var name: String {
get { _name.wrappedValue }
set { _name.wrappedValue = newValue }
}
The wrapper intercepts get and set. You decide what happens in each.
@Clamped — keeping a value within a range
The most useful case I’ve found: clamping a number to a range without repeating that logic in every setter.
@propertyWrapper
struct Clamped<Value: Comparable> {
private var value: Value
let range: ClosedRange<Value>
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = range.clamp(wrappedValue)
}
var wrappedValue: Value {
get { value }
set { value = range.clamp(newValue) }
}
}
extension ClosedRange {
func clamp(_ value: Bound) -> Bound {
min(upperBound, max(lowerBound, value))
}
}
In use:
struct AudioSettings {
@Clamped(0...100) var volume: Int = 50
@Clamped(0.5...3.0) var playbackSpeed: Double = 1.0
}
var settings = AudioSettings()
settings.volume = 150 // stored as 100
settings.volume = -10 // stored as 0
Without the wrapper, that clamp lives in each property’s setter. With it, you write it once and forget about it.
@Trimmed — stripping whitespace automatically
Another one I use when working with user input:
@propertyWrapper
struct Trimmed {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
}
struct UserForm {
@Trimmed var username: String = ""
@Trimmed var email: String = ""
}
var form = UserForm()
form.username = " slekens "
print(form.username) // "slekens"
projectedValue: what you expose with $
In addition to wrappedValue, property wrappers can have a projectedValue, accessible with $. We already saw this in the previous post with $debugOverlay.reset().
You can do the same in your own wrappers. In Clamped, for example, exposing the range:
var projectedValue: ClosedRange<Value> { range }
// Usage:
settings.$volume // returns 0...100
Useful for setting a slider’s range or validating in UI without exposing the full wrapper.
The real cost
The name has to be clear enough that it needs no explanation. @Clamped says it all. @Processed says nothing — and if someone has to go look up what a wrapper does, it’s already lost its reason to exist.
Also: if the logic only applies to one property, a plain setter is more honest. Wrappers earn their keep when the same logic shows up across three, four, five different properties.
The ones Apple gives you
SwiftUI uses property wrappers everywhere: @State, @Binding, @Published, @ObservedObject. They all follow the same pattern. @AppStorage is essentially the @UserDefault from the previous post, wired into SwiftUI’s update cycle.
It’s worth reading their documentation now that you know how they’re built on the inside. They look different.