← Volver al blog

@propertyWrapper en Swift: más allá de UserDefaults

Qué son los property wrappers, cómo construirlos desde cero y cuándo usarlos — con ejemplos prácticos: @Clamped, @Trimmed y projectedValue.

Si ya leíste el post de feature flags, ya usaste un property wrapper. Pero @propertyWrapper puede hacer mucho más que envolver UserDefaults. Es uno de esos features de Swift que, una vez que lo entiendes, empiezas a verlo por todos lados.

Qué es

Un property wrapper es un struct que controla cómo se lee y escribe el valor de una propiedad. El compilador genera el código de acceso por ti — defines las reglas una vez y las reutilizas donde quieras.

Cuando lo vi por primera vez pensé que había algo especial del compilador detrás. No lo hay. Es syntactic sugar sobre un patrón que podrías escribir tú mismo — lo que te ahorra es tener que repetirlo en cada propiedad.

La anatomía

Todo property wrapper necesita una sola cosa: wrappedValue.

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

    var wrappedValue: String {
        get { value }
        set { value = newValue.uppercased() }
    }
}

Cuando escribes @Uppercased var name: String, el compilador lo expande a:

private var _name = Uppercased()
var name: String {
    get { _name.wrappedValue }
    set { _name.wrappedValue = newValue }
}

El wrapper intercepta get y set. Tú decides qué pasa en cada uno.

@Clamped — limitar un valor a un rango

El caso más útil que he encontrado: limitar un número a un rango sin repetir ese clamp en cada setter.

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

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.value = range.clamp(wrappedValue)
    }

    var wrappedValue: Value {
        get { value }
        set { value = range.clamp(newValue) }
    }
}

extension ClosedRange {
    func clamp(_ value: Bound) -> Bound {
        min(upperBound, max(lowerBound, value))
    }
}

En uso:

struct AudioSettings {
    @Clamped(0...100) var volume: Int = 50
    @Clamped(0.5...3.0) var playbackSpeed: Double = 1.0
}

var settings = AudioSettings()
settings.volume = 150   // se guarda como 100
settings.volume = -10   // se guarda como 0

Sin el wrapper, ese clamp vive en el setter de cada propiedad. Con él, lo escribes una vez y te olvidas.

@Trimmed — limpiar espacios automáticamente

Otro que uso cuando trabajo con input del usuario:

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

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

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

var form = UserForm()
form.username = "  slekens  "
print(form.username) // "slekens"

projectedValue: lo que expones con $

Además de wrappedValue, los property wrappers pueden tener un projectedValue, accesible con $. Ya lo vimos en el post anterior con $debugOverlay.reset().

Puedes hacer lo mismo en los tuyos. En Clamped, por ejemplo, exponer el rango:

var projectedValue: ClosedRange<Value> { range }

// Uso:
settings.$volume  // devuelve 0...100

Útil para configurar el rango de un slider o validar en UI sin exponer el wrapper completo.

El costo real

El nombre tiene que ser tan claro que no necesite explicación. @Clamped lo dice todo. @Processed no dice nada — y si alguien tiene que ir a buscar qué hace, el wrapper ya perdió su razón de existir.

También: si la lógica solo aplica a una propiedad, un setter directo es más honesto. Los wrappers pagan su costo cuando la misma lógica aparece en tres, cuatro, cinco propiedades distintas.

Los que ya tienes de Apple

SwiftUI los usa por todos lados: @State, @Binding, @Published, @ObservedObject. Todos siguen el mismo patrón. @AppStorage es básicamente el @UserDefault del post anterior, conectado al ciclo de actualización de SwiftUI.

Vale la pena leer su documentación ahora que ya sabes cómo están hechos por dentro. Se ven distintos.