Swift Property Wrappers

A property wrapper adds a layer of logic around how a property stores and retrieves its value. Instead of writing the same validation or transformation code in every struct or class, you define it once in a property wrapper and reuse it everywhere.

The Smart Outlet Analogy


┌──────────────────────────────────────────────────┐
│  Property Wrapper = A Smart Power Outlet         │
│                                                  │
│  Regular outlet:                                 │
│  Plug in device → device gets power (raw)        │
│                                                  │
│  Smart outlet:                                   │
│  Plug in device → outlet checks voltage,         │
│  regulates it, then delivers safe power          │
│                                                  │
│  The device does not need to worry about         │
│  voltage — the outlet handles it automatically.  │
└──────────────────────────────────────────────────┘

The Problem Without Property Wrappers

struct UserProfile {
    private var _age: Int = 0

    var age: Int {
        get { return _age }
        set { _age = max(0, newValue) }   // validation repeated everywhere
    }

    private var _username: String = ""

    var username: String {
        get { return _username }
        set { _username = newValue.lowercased() }   // same pattern again
    }
}

The same wrapping pattern appears for every property. Property wrappers eliminate this repetition.

Creating a Property Wrapper

@propertyWrapper
struct Clamped {
    private var value: Int
    private 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)
    }
}

The @propertyWrapper attribute marks a struct as a wrapper. The required wrappedValue property is the actual value the wrapper manages.

Using the Wrapper

struct GamePlayer {
    @Clamped(0...100) var health: Int = 100
    @Clamped(1...10)  var level: Int  = 1
}

var player = GamePlayer()
player.health = 150   // clamped to 100
player.health = -30   // clamped to 0
player.level = 15     // clamped to 10

print(player.health)  // 0
print(player.level)   // 10

┌──────────────────────────────────────────────────┐
│  @Clamped in action                              │
│                                                  │
│  health = 150  →  range 0...100  →  stored: 100  │
│  health = -30  →  range 0...100  →  stored: 0    │
│  level  = 15   →  range 1...10   →  stored: 10   │
└──────────────────────────────────────────────────┘

A String Trimming Wrapper

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

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

struct SignupForm {
    @Trimmed var username: String = ""
    @Trimmed var email: String    = ""
}

var form = SignupForm()
form.username = "   swift_user   "
form.email    = "  user@mail.com "

print(form.username)   // swift_user
print(form.email)      // user@mail.com

Whitespace is stripped automatically every time a value is set. The form struct stays clean with no manual trimming calls anywhere.

Projected Value – Extra Information from a Wrapper

@propertyWrapper
struct Logged<T> {
    private var value: T
    private(set) var projectedValue: [T] = []

    var wrappedValue: T {
        get { value }
        set {
            projectedValue.append(newValue)
            value = newValue
        }
    }

    init(wrappedValue: T) {
        self.value = wrappedValue
    }
}

struct Thermometer {
    @Logged var temperature: Double = 20.0
}

var t = Thermometer()
t.temperature = 22.5
t.temperature = 19.0
t.temperature = 24.0

print(t.temperature)    // 24.0     ← current value
print(t.$temperature)   // [22.5, 19.0, 24.0]  ← history via $

The projectedValue is accessed with a $ prefix. This gives you a second, parallel way to read data from the wrapper — here, a full history of changes.

Built-in SwiftUI Property Wrappers

WrapperPurpose
@StateLocal view state that triggers UI refresh
@BindingTwo-way connection to a parent's state
@ObservedObjectWatch an external observable object
@EnvironmentObjectShare data across the entire view hierarchy
@AppStorageSync a property with UserDefaults

SwiftUI is built on property wrappers. Understanding how they work makes reading and writing SwiftUI code much more intuitive.

Leave a Comment

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