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.

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:

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 = 20

This 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 = 120

The 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.$message

The 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

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.$count

Fix: 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.$count

The 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

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 = -5

If 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

11. Practice Exercise

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.