iosNudge

What are property wrappers, and how do you write one?

MediumSwiftSwiftUI 2 min read

TL;DR

A property wrapper moves repeated property logic into one reusable type. You mark a type with @propertyWrapper and give it a wrappedValue. Then you use it as @Name var x. It can also expose a projectedValue, which you read with $x. SwiftUI's @State and @Binding are property wrappers.

The problem it solves

Without a wrapper, you repeat the same logic for many properties:

struct Player {
    private var _health = 100
    var health: Int {
        get { _health }
        set { _health = min(max(newValue, 0), 100) }
    }
}

Imagine this for ten properties.

Writing a wrapper

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

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

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

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

var p = Player()
p.health = 150
print(p.health)   // 100

What the compiler generates

// You write:
@Clamped(0...100) var health = 100

// Swift creates roughly:
private var _health = Clamped(wrappedValue: 100, 0...100)
var health: Int {
    get { _health.wrappedValue }
    set { _health.wrappedValue = newValue }
}

_health is the wrapper itself. It is private to the type.

Projected value: $name

A wrapper can expose extra data through projectedValue.

@propertyWrapper
struct Trimmed {
    private(set) var projectedValue = false   // true if trimming changed the text
    private var value = ""

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

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

struct Form {
    @Trimmed var email = ""
}

var f = Form()
f.email = "  a@b.com "
print(f.email)    // "a@b.com"
print(f.$email)   // true

In SwiftUI, $text on a @State gives a Binding<String>. That is its projected value.

Wrappers you already use

WrapperWhereWhat it does
@StateSwiftUIView-owned state
@BindingSwiftUIRead/write reference to someone else's state
@EnvironmentSwiftUIRead a value from the environment
@AppStorageSwiftUIRead/write UserDefaults
@PublishedCombineEmits changes in an ObservableObject

Interviewers sometimes check that you know @MainActor and @Observable are not property wrappers. @MainActor is a global actor. @Observable is a macro.

Where you can use them

  • Stored properties in structs and classes.
  • Local variables (Swift 5.4+).
  • Function and closure parameters (Swift 5.5+).

You cannot use a wrapper on a computed property. You cannot declare one in a protocol requirement.

Limits and gotchas

  • A wrapped property cannot be lazy, weak, or unowned.
  • @Published works only inside classes.
  • The wrapper is not thread-safe by default. A wrapper with a lock inside does not make x += 1 atomic, because that is a read followed by a write.
  • Wrappers can hide logic. Keep them small and obvious.

Common follow-up questions

  • What is the difference between a property wrapper and a macro? A wrapper is a runtime type that stores the value. A macro generates code at compile time. @Observable replaced @Published style wrappers for observation.
  • Can a wrapper access the object that contains it? Not through the public API. There is an underscored static subscript (_enclosingInstance) that @Published uses, but it is not official.
  • Why use init(wrappedValue:)? It lets users write @Clamped(0...100) var health = 100 with a normal default value.
  • How is @State different from a normal wrapper? SwiftUI stores the real value outside the view struct, so it survives when the view is recreated.
  • Can you nest wrappers? Yes, like @A @B var x. It works, but it is hard to read, so it is rare.