Property wrappers in Swift: patterns, pitfalls and fixes
Property wrappers are one of Swift's most powerful yet underutilized features for encapsulating reusable property behavior. Once you've built a few custom wrappers—beyond just using SwiftUI's @State—you unlock a pattern that eliminates boilerplate, enforces invariants, and makes your codebase more maintainable. In this article, we'll walk through how property wrappers work, where they shine, and the mistakes that catch teams when they try to push them too far.
Understanding @propertyWrapper with wrappedValue
At its core, a property wrapper is a struct or class that wraps a value and adds custom behavior around getting and setting it. The @propertyWrapper attribute tells Swift to treat your type specially: when you use it on a property, Swift rewrites that property to use your wrapper's logic.
Every property wrapper must have a wrappedValue property. This is the actual value being wrapped. Let's build a simple example: a Positive wrapper that ensures a number never goes below 1.
@propertyWrapper
struct Positive {
private var _value: Int = 1
var wrappedValue: Int {
get { _value }
set { _value = max(1, newValue) }
}
init(wrappedValue: Int) {
self._value = max(1, wrappedValue)
}
}
struct Product {
@Positive var quantity: Int = 5
}
var product = Product()
product.quantity = -10 // Silently clamped to 1
print(product.quantity) // Prints: 1
When you write @Positive var quantity: Int = 5, Swift transforms it behind the scenes. The property is no longer a simple stored property; it becomes an instance of the Positive struct, and any access to quantity goes through the wrapper's getter and setter. This is remarkably powerful: you've added validation logic without changing the call site.
The init(wrappedValue:) initializer is required. Swift calls it automatically when you initialize a wrapped property. If you don't provide one, you'll get a compiler error. The initialization value becomes the wrappedValue parameter.
How SwiftUI's @State is a property wrapper
You've been using property wrappers all along if you've worked with SwiftUI. The @State annotation is a property wrapper that manages a value's lifecycle and automatically triggers view redraws when it changes.
struct LoginView: View {
@State private var username: String = ""
@State private var password: String = ""
@State private var isLoading: Bool = false
var body: some View {
VStack {
TextField("Username", text: $username)
SecureField("Password", text: $password)
Button("Login") {
Task {
isLoading = true
await login()
isLoading = false
}
}
if isLoading {
ProgressView()
}
}
}
private func login() async {
// Network call
}
}
Here, @State owns the storage for username, password, and isLoading. When you modify these values, SwiftUI's State wrapper notifies the view system that a redraw is needed. Without property wrappers, you'd need a separate view model and manual observation logic.
SwiftUI provides several built-in wrappers: @State for local view state, @Binding for passing state down to child views, @StateObject for reference types, and @EnvironmentObject for app-wide state. Each one encapsulates a different ownership and observation pattern. This is why property wrappers are the backbone of SwiftUI architecture.
Projected values with the $ syntax
Property wrappers can expose a secondary value called a projected value using the projectedValue property. When you prefix a wrapped property with $, you get the projected value instead of the wrapped value. In SwiftUI, this is how $username gives you a Binding instead of a String.
Let's build a wrapper that provides both the actual value and a "is valid" flag:
@propertyWrapper
struct Validated {
private var _value: String = ""
var wrappedValue: String {
get { _value }
set { _value = newValue }
}
var projectedValue: ValidationProxy {
ValidationProxy(getter: { _value }, setter: { _value = $0 })
}
init(wrappedValue: String) {
self._value = wrappedValue
}
}
struct ValidationProxy {
let getter: () -> String
let setter: (String) -> Void
var isEmailValid: Bool {
getter().contains("@")
}
var isEmpty: Bool {
getter().trimmingCharacters(in: .whitespaces).isEmpty
}
}
struct SignupForm {
@Validated var email: String = ""
func validateBeforeSubmit() -> Bool {
return !$email.isEmpty && $email.isEmailValid
}
}
let form = SignupForm()
print(form.$email.isEmpty) // Prints: true
Now form.email gives you the string value, but form.$email gives you a validation proxy with helper methods. This pattern is especially useful when you want to expose read-only derived state or helper functions alongside your wrapped value.
SwiftUI leans heavily on projected values. When you write TextField("Name", text: $name), you're passing the projected value (a Binding) to the text field, not the raw String. The Binding allows the field to both read and write the value. Without this, there'd be no way to link the text field to your @State variable.
Use for clamping and validation
One of the most practical uses for property wrappers is enforcing invariants through clamping and validation. Instead of scattering guard statements and validations throughout your code, you centralize the logic in a wrapper.
Here's a more realistic example: a price wrapper that ensures a value stays within acceptable bounds and always rounds to two decimal places.
@propertyWrapper
struct Price {
private var _cents: Int = 0
var wrappedValue: Decimal {
get {
Decimal(_cents) / 100
}
set {
let clamped = max(0, min(999_999_99, newValue))
_cents = Int(clamped * 100)
}
}
var projectedValue: PriceInfo {
PriceInfo(
cents: _cents,
formattedUSD: formatAsUSD(Decimal(_cents) / 100)
)
}
init(wrappedValue: Decimal) {
self.wrappedValue = wrappedValue
}
private func formatAsUSD(_ value: Decimal) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = "USD"
return formatter.string(from: NSDecimalNumber(decimal: value)) ?? "$0.00"
}
}
struct PriceInfo {
let cents: Int
let formattedUSD: String
}
struct CartItem {
let name: String
@Price var unitPrice: Decimal = 0
@Price var discount: Decimal = 0
var finalPrice: Decimal {
unitPrice - discount
}
}
var item = CartItem(name: "Widget")
item.unitPrice = 19.99
item.discount = -5 // Clamped to 0
item.unitPrice = 1_000_000 // Clamped to 9_999.99
print(item.$unitPrice.formattedUSD) // Prints: $9999.99
By using a property wrapper, every assignment to unitPrice or discount automatically validates and clamps the value. There's no way to accidentally set an invalid price. The logic lives in one place and is reusable across your entire codebase.
Another practical pattern: user defaults integration. Rather than sprinkling UserDefaults.standard.set() and UserDefaults.standard.data(forKey:) throughout your code, wrap it:
@propertyWrapper
struct AppStorage {
let key: String
let defaultValue: String
var wrappedValue: String {
get {
UserDefaults.standard.string(forKey: key) ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
init(wrappedValue: String, key: String) {
self.key = key
self.defaultValue = wrappedValue
}
}
class UserPreferences {
@AppStorage(key: "theme") var theme: String = "light"
@AppStorage(key: "language") var language: String = "en"
}
let prefs = UserPreferences()
prefs.theme = "dark" // Automatically persisted to UserDefaults
Common mistakes
Forgetting the init(wrappedValue:) initializer. Property wrappers require this—it's how Swift knows how to initialize the wrapper when you assign a value. Without it, you'll see mysterious compiler errors about the wrapper not conforming to the expected protocol.
Mutating projectedValue in a struct wrapper. If your wrapper is a struct and your projected value property tries to mutate the wrapper, you'll hit issues. Struct methods can't mutate self through a getter. This is why many wrappers use reference types (classes) for their projected values, or avoid mutating operations in the projection.
Using property wrappers for logic that belongs in a computed property. Not everything needs a wrapper. If you just need a computed value with no stored state, use var with a getter instead. Wrappers add cognitive load; use them only when you need to encapsulate storage behavior or enforce invariants.
Overcomplicating nested wrappers. You can compose property wrappers—use @StateObject inside a @State, for example—but each layer adds complexity. If you're nesting more than two wrappers, step back and consider whether a simpler design exists.
Ignoring Sendability with async/await. If you're using property wrappers with concurrency, ensure your wrapper is Sendable if it might cross isolation boundaries. This is increasingly important as Swift's concurrency model matures. A wrapper with mutable reference types as storage can cause data races.
Key takeaways
- Property wrappers encapsulate reusable property behavior via a
wrappedValueproperty; Swift rewrites wrapped properties to route access through the wrapper. - Every property wrapper needs an
init(wrappedValue:)initializer so Swift knows how to initialize it. - Projected values, accessed with the
$prefix, expose secondary behavior like validation flags or Bindings alongside the wrapped value. - SwiftUI's core state management—
@State,@Binding,@StateObject—are all property wrappers; understanding how they work unlocks deeper SwiftUI knowledge. - Use property wrappers for clamping, validation, and persistence to centralize invariant-enforcing logic and eliminate boilerplate.
- Avoid overusing them; simple computed properties often do the job without the extra machinery.
- Be careful with struct wrappers that expose mutable projected values—struct semantics can surprise you.






Comments
Loading comments…