Property wrappers are a Swift feature that allows you to define reusable logic for getting and setting property values. They reduce boilerplate code and enable elegant solutions for common patterns like lazy initialization, validation, and persistence. This post is a deep dive suited for intermediate to advanced Swift developers who already have basic familiarity with property wrappers.

What are Property Wrappers?

Property wrappers add a layer of separation between code that manages how a property is stored and the code that defines a property. They're defined using the @propertyWrapper attribute.

Creating a Simple Property Wrapper

Here's a basic example that ensures a value stays within bounds:

@propertyWrapper
struct Clamped<Value: Comparable> {
    private var value: Value
    private let range: ClosedRange<Value>

    var wrappedValue: Value {
        get { value }
        set { value = min(max(range.lowerBound, newValue), range.upperBound) }
    }

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.value = min(max(range.lowerBound, wrappedValue), range.upperBound)
    }
}

struct GameCharacter {
    @Clamped(0...100) var health: Int = 100
    @Clamped(0...100) var mana: Int = 50
}

var character = GameCharacter()
character.health = 150  // Clamped to 100
character.mana = -10    // Clamped to 0

Projected Values

Property wrappers can provide additional functionality through projected values:

@propertyWrapper
struct Capitalized {
    private var value: String

    var wrappedValue: String {
        get { value }
        set { value = newValue }
    }

    var projectedValue: String {
        value.uppercased()
    }

    init(wrappedValue: String) {
        self.value = wrappedValue.capitalized
    }
}

struct User {
    @Capitalized var name: String
}

var user = User(name: "john doe")
print(user.name)    // "John Doe"
print(user.$name)   // "JOHN DOE" (projected value)

UserDefaults Property Wrapper

A practical example for persisting values:

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    var wrappedValue: T {
        get {
            UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

struct AppSettings {
    @UserDefault(key: "isDarkMode", defaultValue: false)
    var isDarkMode: Bool

    @UserDefault(key: "username", defaultValue: "")
    var username: String

    @UserDefault(key: "fontSize", defaultValue: 16.0)
    var fontSize: Double
}

var settings = AppSettings()
settings.isDarkMode = true  // Automatically persisted

Validation Property Wrapper

Validate property values on assignment:

@propertyWrapper
struct Validated<Value> {
    private var value: Value
    private let validator: (Value) -> Bool

    var wrappedValue: Value {
        get { value }
        set {
            guard validator(newValue) else {
                print("Validation failed")
                return
            }
            value = newValue
        }
    }

    init(wrappedValue: Value, validator: @escaping (Value) -> Bool) {
        self.validator = validator
        self.value = wrappedValue
    }
}

struct EmailForm {
    @Validated(validator: { $0.contains("@") && $0.contains(".") })
    var email: String = ""
}

var form = EmailForm()
form.email = "invalid"          // Validation failed
form.email = "user@example.com" // Valid

Combining Property Wrappers

You can stack multiple property wrappers:

@propertyWrapper
struct Trimmed {
    private var value: String = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
    }
}

struct Form {
    @UserDefault(key: "name", defaultValue: "")
    @Trimmed
    var name: String
}

SwiftUI Built-in Property Wrappers

SwiftUI extensively uses property wrappers:

  • @State - Local state management

  • @Binding - Two-way bindings

  • @ObservedObject - Observable object references

  • @EnvironmentObject - Environment-provided objects

  • @Environment - Environment values

Best Practices

  1. Keep wrappers focused - Each wrapper should handle one responsibility

  2. Document behavior - Make it clear what the wrapper does

  3. Consider performance - Avoid heavy operations in getters/setters

  4. Provide good defaults - Make the wrapper easy to use

  5. Use projected values - For additional related functionality

Wrap up

Property wrappers are a powerful tool for reducing boilerplate and creating reusable property behavior. They enable clean, declarative code and are fundamental to SwiftUI's design.

Resources:

Keep reading

Read more

6 min read Differentiate Without Color Read article 2 min read Monospace digits Read article 5 min read ContentUnavailableView Read article

Share

Share this article

Share Bluesky Mastodon Twitter LinkedIn Facebook