@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.