Swift Property Wrappers: A Practical Guide to @propertyWrapper
Swift property wrappers let you attach reusable logic to a property so you can validate, transform, observe, or store values in a consistent way. They are a language feature, not a framework feature, and they help remove repetitive code from models, view state, and data validation.
Quick answer: A property wrapper is a type marked with @propertyWrapper that defines how a property gets and sets its value through wrappedValue. Use it when the same storage or validation logic would otherwise be repeated across many properties.
Difficulty: Intermediate
You'll understand this better if you know: basic Swift properties, structs and classes, and how getters and setters work.
1. What Is Swift Property Wrappers?
A property wrapper is a reusable type that sits between a property and the code that reads or writes it. Instead of putting validation or conversion logic directly into every property, you define that behavior once in a wrapper and apply it with an attribute such as @Clamped or @Trimmed.
- They are declared with the @propertyWrapper attribute.
- They control a property's storage through wrappedValue.
- They can expose extra information through projectedValue.
- They are useful for validation, defaults, formatting, caching, and observation.
- They reduce duplicated accessor code in models and APIs.
Think of a property wrapper as a small reusable layer around a property value. The wrapped property still looks like a normal property at the call site, but the wrapper handles the implementation details.
2. Why Swift Property Wrappers Matter
Without wrappers, repeated property behavior often turns into duplicated didSet logic, custom getters and setters, or helper methods scattered across your types. Property wrappers make that behavior composable and easier to read.
They matter because they improve:
- Consistency: the same rules are applied everywhere the wrapper is used.
- Readability: the property declaration shows the intent clearly.
- Reuse: one wrapper can serve many properties.
- Encapsulation: validation and storage details stay inside the wrapper type.
They are especially helpful when you want a property to always stay within a valid range, automatically trim text, supply a default value, or publish changes to observers.
3. Basic Syntax or Core Idea
A property wrapper is a type that defines at least a wrappedValue property. You apply it to another property with the wrapper name prefixed by @.
Minimal wrapper example
The following wrapper stores an integer and prints it back unchanged. It is intentionally simple so you can see the moving parts clearly.
@propertyWrapper
struct Logged {
var wrappedValue: Int
}
struct User {
@Logged var score: Int
}
var user = User(score: 10)
user.score = 20This code creates a wrapper type named Logged, then uses it on score. Swift stores the value through the wrapper, but the property still reads and writes like a normal Int.
How wrappedValue works
The wrappedValue property is the value exposed to code outside the wrapper. When you read user.score, Swift gives you the wrapper's wrappedValue. When you assign to user.score, Swift updates that same value.
4. Step-by-Step Examples
Example 1: Clamping a number
A common use case is forcing a value into a safe range. This wrapper ensures a volume value always stays between 0 and 100.
@propertyWrapper
struct Clamped {
private var value: Int
let range: ClosedRange<Int>
var wrappedValue: Int {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: Int, _ range: ClosedRange<Int>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
struct PlayerSettings {
@Clamped(0...100) var volume: Int = 150
}
var settings = PlayerSettings()
settings.volume = 120The initial value and later assignments are both clamped. That means volume never leaves the allowed range.
Example 2: Trimming text input
Wrappers are often useful for cleaning user input before it enters your model.
@propertyWrapper
struct Trimmed {
private var value: String
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
init(wrappedValue: String) {
self.value = wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
struct Profile {
@Trimmed var username: String
}
var profile = Profile(username: " alice ")The wrapper keeps whitespace out of the stored value, which helps avoid subtle bugs in comparisons, storage, and validation.
Example 3: Providing a default value
A wrapper can also provide a useful default when no explicit value is passed.
@propertyWrapper
struct DefaultFalse {
var wrappedValue: Bool = false
}
struct FeatureFlags {
@DefaultFalse var isEnabled: Bool
}
let flags = FeatureFlags()This example shows a wrapper with a built-in default. The property can be declared without an explicit initializer at the use site.
Example 4: Accessing projectedValue
Some wrappers expose extra information through projectedValue. By convention, that value is read through the $ prefix.
@propertyWrapper
struct LoggedChange<Value> {
private var value: Value
var wrappedValue: Value {
get { value }
set { value = newValue }
}
var projectedValue: String {
"Current value is set"
}
init(wrappedValue: Value) {
self.value = wrappedValue
}
}
struct AuditRecord {
@LoggedChange var message: String = "Saved"
}
let record = AuditRecord()
let status = record.$messageThe projected value is a second interface for the wrapper. In real code it might expose a binding, metadata, validation state, or a helper object.
5. Practical Use Cases
- Validating form values such as email addresses, usernames, or numeric limits.
- Normalizing text by trimming whitespace or standardizing case.
- Applying safe defaults to configuration and feature flags.
- Wrapping persistence logic, such as storing values in user defaults or a cache.
- Exposing auxiliary state through projectedValue for debugging or binding.
- Reducing repeated boilerplate in models with many similar properties.
Use property wrappers when the rule belongs to the property itself, not just to a single method call. If the logic should always apply whenever the value changes, a wrapper is often a good fit.
6. Common Mistakes
Mistake 1: Forgetting to provide a wrappedValue initializer
Swift needs a way to construct the wrapper when you use it on a property. If the wrapper stores data but never defines how to initialize wrappedValue, property declaration can fail or become awkward.
Problem: The wrapper type has storage, but the compiler cannot match the wrapped property to a valid initialization path.
@propertyWrapper
struct Title {
private var value: String
}
struct Book {
@Title var name: String
}Fix: Add an initializer that accepts wrappedValue and stores it in the wrapper.
@propertyWrapper
struct Title {
private var value: String
var wrappedValue: String {
get { value }
set { value = newValue }
}
init(wrappedValue: String) {
self.value = wrappedValue
}
}
struct Book {
@Title var name: String
}The corrected version works because Swift can now create the wrapper from the property's initial value.
Mistake 2: Expecting projectedValue to exist automatically
The $ accessor only exists when the wrapper defines projectedValue. New Swift users sometimes assume every wrapped property automatically has a projected value.
Problem: Accessing $property on a wrapper that does not define projectedValue results in a compile-time error.
@propertyWrapper
struct ClampedInt {
var wrappedValue: Int
}
struct Settings {
@ClampedInt var count: Int = 1
}
let settings = Settings()
let info = settings.$countFix: Define projectedValue if you want the $ syntax.
@propertyWrapper
struct ClampedInt {
var wrappedValue: Int
var projectedValue: String {
"The value is " + String(wrappedValue)
}
}
struct Settings {
@ClampedInt var count: Int = 1
}
let settings = Settings()
let info = settings.$countThe corrected version works because the wrapper now explicitly exposes the projected value.
Mistake 3: Using a wrapper when the value should not be transformed
Wrappers are powerful, but they should not hide surprising behavior. If a property needs to preserve exact input, automatic changes such as trimming or clamping may be the wrong choice.
Problem: The wrapper silently changes the value, which can lead to confusing bugs when the stored value does not match the input you passed in.
@propertyWrapper
struct Lowercased {
private var value: String
var wrappedValue: String {
get { value }
set { value = newValue.lowercased() }
}
init(wrappedValue: String) {
self.value = wrappedValue.lowercased()
}
}
struct Tag {
@Lowercased var label: String
}
let tag = Tag(label: "SwiftUI")Fix: Use a wrapper only when the transformation is a true rule of the domain, or remove the wrapper and store the exact value directly.
struct Tag {
var label: String
}
let tag = Tag(label: "SwiftUI")The corrected version works because the property stores the value exactly as provided, without hidden conversion.
7. Best Practices
Practice 1: Keep wrapper behavior small and focused
A wrapper should usually do one job well. If it starts validating, logging, caching, and formatting all at once, it becomes hard to reason about and harder to reuse.
@propertyWrapper
struct Trimmed {
private var value: String
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
init(wrappedValue: String) {
self.value = wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines)
}
}This wrapper is easy to reuse because its responsibility is clear and limited.
Practice 2: Make the transformed behavior obvious in the property name
If a wrapper changes input values, the property name should make that behavior predictable. This helps prevent surprises for other developers reading the model.
struct Account {
@Trimmed var displayName: String
}Here, displayName is a reasonable place for trimming because the name suggests a user-facing string, not an exact raw input record.
Practice 3: Prefer initialization that preserves invariants
Ensure the wrapper's initializer enforces the same rules as later assignments. Otherwise, the property can start in an invalid state and only become valid after the first update.
@propertyWrapper
struct Clamped {
private var value: Int
let range: ClosedRange<Int>
var wrappedValue: Int {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: Int, _ range: ClosedRange<Int>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}This keeps the property valid from the moment it is created, which is especially important for model integrity.
8. Limitations and Edge Cases
- Wrappers add a layer of abstraction, which can make debugging slightly less direct if the behavior is not documented clearly.
- A wrapper is not always the best choice for one-off logic that only applies to a single property.
- Wrapped properties still obey the normal rules of the enclosing type, including access control and mutation rules.
- If a wrapper is applied to a computed property, the design is usually wrong; wrappers are meant for stored properties.
- Using wrappers on value types and reference types can feel different when the wrapper itself stores mutable state.
- Some wrapper designs expose additional capabilities through projectedValue, but not every wrapper needs that complexity.
One common surprise is that the wrapper's own storage and the wrapped property's visible value are not necessarily the same conceptual thing. The wrapper may keep private state, while the wrapped property exposes only the public interface.
9. Practical Mini Project
Let's build a small profile model that uses two wrappers: one trims names and one clamps age to a safe range. This is a realistic pattern for user input coming from a form or import step.
@propertyWrapper
struct Trimmed {
private var value: String
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
init(wrappedValue: String) {
self.value = wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
@propertyWrapper
struct Clamped {
private var value: Int
let range: ClosedRange<Int>
var wrappedValue: Int {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: Int, _ range: ClosedRange<Int>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
struct Profile {
@Trimmed var displayName: String
@Clamped(0...130) var age: Int
}
var profile = Profile(displayName: " Sam ", age: 200)
profile.displayName = " Taylor "
profile.age = -5If you inspect profile after these assignments, the name will be trimmed and the age will be clamped into range. This is a compact example of how wrappers keep model rules close to the data they protect.
10. Key Points
- Property wrappers move repeated property behavior into a reusable type.
- The wrapped property is exposed through wrappedValue.
- The $ prefix reads projectedValue when the wrapper defines it.
- They are best for rules that should always apply to a property's value.
- Clear naming and simple behavior make wrappers easier to maintain.
11. Practice Exercise
- Create a wrapper named NonEmpty that stores a String.
- Make it replace an empty string with "Unnamed".
- Use it in a Contact struct for a nickname property.
- Print the stored nickname after initialization and after assigning an empty string.
Expected output: the nickname should never be empty.
Hint: Put the normalization logic in both the setter and the wrapper initializer.
Solution:
@propertyWrapper
struct NonEmpty {
private var value: String
var wrappedValue: String {
get { value }
set { value = newValue.isEmpty ? "Unnamed" : newValue }
}
init(wrappedValue: String) {
self.value = wrappedValue.isEmpty ? "Unnamed" : wrappedValue
}
}
struct Contact {
@NonEmpty var nickname: String
}
var contact = Contact(nickname: "")
print(contact.nickname)
contact.nickname = ""
print(contact.nickname)12. Final Summary
Swift property wrappers are a clean way to package repeated property behavior into a reusable type. They are most useful when a property should always follow a rule such as trimming, clamping, defaulting, or exposing related metadata.
To use them well, keep each wrapper focused, make its effect easy to understand, and initialize values in a way that preserves the same rules you apply later. When used carefully, wrappers make your Swift code shorter, safer, and easier to maintain.
If you want to go further, next study projectedValue patterns, wrapper composition, and how property wrappers appear in SwiftUI state types.