Swift Property Wrappers

A property wrapper adds a layer of logic between a property and the code that stores or reads it. Instead of repeating validation, clamping, or storage logic in every property, you write it once in a wrapper and reuse it anywhere. Property wrappers power SwiftUI's @State, @Binding, @Published, and many other common tools.

The Problem They Solve

Suppose every temperature property in your app must stay between -50 and 150. Without a wrapper, you repeat the clamping logic everywhere:

struct Weather {
    private var _temperature: Double = 0
    var temperature: Double {
        get { _temperature }
        set { _temperature = max(-50, min(150, newValue)) }
    }
}

struct Engine {
    private var _heatLevel: Double = 0
    var heatLevel: Double {
        get { _heatLevel }
        set { _heatLevel = max(-50, min(150, newValue)) }
    }
}
// Same logic written twice — and growing

Creating a Property Wrapper

Mark a struct, class, or enum with @propertyWrapper. Implement a computed property named exactly wrappedValue — that is the actual storage or logic gate.

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

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

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

Using the Wrapper

struct Weather {
    @Clamped(range: -50...150) var temperature: Double = 20
}

var w = Weather()
print(w.temperature)   // Output: 20.0

w.temperature = 200    // Over the maximum
print(w.temperature)   // Output: 150.0

w.temperature = -100   // Under the minimum
print(w.temperature)   // Output: -50.0

Diagram: Property Wrapper Flow

Code writes:   w.temperature = 200
                      |
               Clamped.wrappedValue.set(200)
                      |
               min(max(200, -50), 150) = 150
                      |
               Stored value = 150

Code reads:    w.temperature
                      |
               Clamped.wrappedValue.get()
                      |
               Returns 150

A Capitalized Wrapper

@propertyWrapper
struct Capitalized {
    private var text = ""

    var wrappedValue: String {
        get { text }
        set { text = newValue.capitalized }
    }
}

struct UserProfile {
    @Capitalized var firstName: String
    @Capitalized var lastName: String
}

var profile = UserProfile()
profile.firstName = "alice"
profile.lastName  = "johnson"

print(profile.firstName)   // Output: Alice
print(profile.lastName)    // Output: Johnson

projectedValue — The $ Accessor

A wrapper can expose a second value through a projectedValue property. Access it with a $ prefix. SwiftUI's @State uses this to expose a Binding.

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

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

    var projectedValue: Bool {
        return value.count >= 6
    }
}

struct LoginForm {
    @Validated var password: String
}

var form = LoginForm()
form.password = "abc"
print(form.$password)    // Output: false  (less than 6 chars)

form.password = "securepass"
print(form.$password)    // Output: true

Common Built-In Property Wrappers

WrapperWhere UsedWhat It Does
@StateSwiftUILocal view state; triggers re-render on change
@BindingSwiftUITwo-way connection to a parent's state
@PublishedCombinePublishes changes to subscribers
@EnvironmentSwiftUIReads values injected from the environment
@AppStorageSwiftUIPersists values in UserDefaults automatically
@ObservedObjectSwiftUIWatches an external observable object for changes

UserDefaults Wrapper Example

A practical wrapper that saves and loads values from UserDefaults automatically:

@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: "Guest")
    var username: String
}

var settings = AppSettings()
print(settings.username)     // Output: Guest

settings.username = "Alice"
print(settings.username)     // Output: Alice (saved to UserDefaults)

Wrapper Initialization Rules

With a Default Value

struct Foo {
    @Clamped(range: 0...100) var score: Double = 50
    // Calls: Clamped(wrappedValue: 50, range: 0...100)
}

Without a Default — Set in init

struct Bar {
    @Clamped(range: 0...100) var score: Double

    init(score: Double) {
        self._score = Clamped(wrappedValue: score, range: 0...100)
    }
}

Access the wrapper itself (not the wrapped value) through the underscore-prefixed name: _score.

Summary

Property wrappers encapsulate reusable property logic — validation, clamping, formatting, persistence — behind the @ annotation syntax. Define them with @propertyWrapper and a wrappedValue computed property. Add a projectedValue to expose extra information via the $ accessor. SwiftUI's entire state management system is built on property wrappers, making this a foundational concept for iOS development.

Leave a Comment

Your email address will not be published. Required fields are marked *