Los property wrappers y los result builders son dos mecanismos de metaprogramación que Swift usa internamente para implementar SwiftUI, Combine y el nuevo Testing framework. Entenderlos no solo aclara cómo funcionan esas APIs, sino que permite crear abstracciones propias igual de expresivas: desde envoltorios de UserDefaults hasta lenguajes de dominio específico (DSLs) con sintaxis declarativa.
Property wrappers: encapsular lógica de acceso
Un property wrapper añade lógica de acceso a una propiedad sin que el código que usa esa propiedad necesite saber que existe. Se define con @propertyWrapper y debe tener una propiedad wrappedValue:
@propertyWrapper
struct Clamped<T: Comparable> {
private var valor: T
let rango: ClosedRange<T>
init(wrappedValue: T, _ rango: ClosedRange<T>) {
self.rango = rango
self.valor = min(max(wrappedValue, rango.lowerBound), rango.upperBound)
}
var wrappedValue: T {
get { valor }
set { valor = min(max(newValue, rango.lowerBound), rango.upperBound) }
}
}
struct Configuracion {
@Clamped(0...100) var volumen: Int = 50
@Clamped(8...72) var tamanoFuente: Double = 16
@Clamped(-90.0...90.0) var latitud: Double = 0
}
var config = Configuracion()
config.volumen = 150 // Se clampea a 100
config.volumen = -10 // Se clampea a 0
print(config.volumen) // 0
@UserDefault: persistencia automática
@propertyWrapper
struct UserDefault<T> {
let clave: String
let porDefecto: T
let suite: UserDefaults
init(_ clave: String, default porDefecto: T, suite: UserDefaults = .standard) {
self.clave = clave
self.porDefecto = porDefecto
self.suite = suite
}
var wrappedValue: T {
get { suite.object(forKey: clave) as? T ?? porDefecto }
set {
if let opcional = newValue as? AnyOptional, opcional.isNil {
suite.removeObject(forKey: clave)
} else {
suite.set(newValue, forKey: clave)
}
}
}
}
// Helper para detectar nil en genéricos
protocol AnyOptional { var isNil: Bool { get } }
extension Optional: AnyOptional { var isNil: Bool { self == nil } }
struct Preferencias {
@UserDefault("tema", default: "sistema")
static var tema: String
@UserDefault("notificaciones_activadas", default: true)
static var notificacionesActivadas: Bool
@UserDefault("ultimo_usuario_id", default: nil)
static var ultimoUsuarioId: Int?
}
Preferencias.tema = "oscuro"
print(Preferencias.tema) // "oscuro" persiste entre sesiones
@Trimmed: transformación automática
@propertyWrapper
struct Trimmed {
private var valor: String = ""
init(wrappedValue: String) {
valor = wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines)
}
var wrappedValue: String {
get { valor }
set { valor = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
}
struct Formulario {
@Trimmed var nombre: String = ""
@Trimmed var email: String = ""
}
var form = Formulario()
form.nombre = " Ana García "
print(form.nombre) // "Ana García" sin espacios
projectedValue: acceso adicional con $
Además de wrappedValue, un property wrapper puede exponer un projectedValue accessible con el prefijo $:
@propertyWrapper
struct Validado {
private var valor: String
private(set) var projectedValue: Bool = false // Accesible como $campo
init(wrappedValue: String) {
valor = wrappedValue
projectedValue = esValido(wrappedValue)
}
var wrappedValue: String {
get { valor }
set {
valor = newValue
projectedValue = esValido(newValue)
}
}
private func esValido(_ s: String) -> Bool { s.count >= 3 }
}
struct FormularioRegistro {
@Validado var nombre: String = ""
@Validado var apellido: String = ""
}
var form = FormularioRegistro()
form.nombre = "An"
print(form.$nombre) // false menos de 3 caracteres
form.nombre = "Ana"
print(form.$nombre) // true
Result builders: DSLs con sintaxis declarativa
Un result builder permite escribir código que parece imperativo pero que el compilador transforma en una construcción declarativa. SwiftUI lo usa con @ViewBuilder.
@resultBuilder
struct HTMLBuilder {
// Una sola expresión
static func buildExpression(_ elemento: HTMLElemento) -> [HTMLElemento] {
[elemento]
}
// Múltiples componentes en secuencia
static func buildBlock(_ componentes: [HTMLElemento]...) -> [HTMLElemento] {
componentes.flatMap { $0 }
}
// Soporte para if
static func buildOptional(_ componente: [HTMLElemento]?) -> [HTMLElemento] {
componente ?? []
}
// Soporte para if/else
static func buildEither(first componente: [HTMLElemento]) -> [HTMLElemento] {
componente
}
static func buildEither(second componente: [HTMLElemento]) -> [HTMLElemento] {
componente
}
// Soporte para for
static func buildArray(_ componentes: [[HTMLElemento]]) -> [HTMLElemento] {
componentes.flatMap { $0 }
}
}
// Tipos básicos del DSL
protocol HTMLElemento {
func render() -> String
}
struct Div: HTMLElemento {
let clase: String?
let hijos: [HTMLElemento]
init(clase: String? = nil, @HTMLBuilder contenido: () -> [HTMLElemento]) {
self.clase = clase
self.hijos = contenido()
}
func render() -> String {
let claseAttr = clase.map { " class="($0)"" } ?? ""
return "<div(claseAttr)>(hijos.map { $0.render() }.joined())</div>"
}
}
struct P: HTMLElemento {
let texto: String
func render() -> String { "<p>(texto)</p>" }
}
struct H1: HTMLElemento {
let texto: String
func render() -> String { "<h1>(texto)</h1>" }
}
// Uso del DSL
let usuarios = ["Ana", "Luis", "María"]
let html = Div(clase: "contenedor") {
H1(texto: "Lista de usuarios")
P(texto: "Total: (usuarios.count)")
for usuario in usuarios {
P(texto: usuario)
}
}
print(html.render())
// <div class="contenedor"><h1>Lista de usuarios</h1><p>Total: 3</p><p>Ana</p>...
Métodos opcionales del result builder
@resultBuilder
struct ArrayBuilder<T> {
static func buildExpression(_ e: T) -> [T] { [e] }
static func buildBlock(_ cs: [T]...) -> [T] { cs.flatMap { $0 } }
static func buildOptional(_ c: [T]?) -> [T] { c ?? [] }
static func buildEither(first c: [T]) -> [T] { c }
static func buildEither(second c: [T]) -> [T] { c }
static func buildArray(_ cs: [[T]]) -> [T] { cs.flatMap { $0 } }
// Para 'if #available':
static func buildLimitedAvailability(_ c: [T]) -> [T] { c }
}
func construirMenu(@ArrayBuilder<MenuItem> items: () -> [MenuItem]) -> Menu {
Menu(items: items())
}
let menu = construirMenu {
MenuItem(titulo: "Inicio", icono: "house")
MenuItem(titulo: "Perfil", icono: "person")
if esAdmin {
MenuItem(titulo: "Admin", icono: "gear")
}
for seccion in secciones {
MenuItem(titulo: seccion.nombre, icono: seccion.icono)
}
}
Resumen
Los property wrappers encapsulan lógica de acceso que de otro modo se repetiría en cada propiedad: validación, transformación, persistencia en UserDefaults o sincronización con sistemas externos. Los result builders transforman código en apariencia imperativo con if, for y expresiones en secuencia en construcciones de datos tipadas. Juntos, son los bloques que Apple usa para construir SwiftUI, y que tú puedes usar para crear APIs igual de expresivas en tus proyectos.
