Property Wrappers y Result Builders en Swift: crear DSLs y abstracciones de comportamiento

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP