Property Wrapper es un mecanismo de Swift que añade una capa de lógica al acceso y modificación de propiedades sin duplicar código. En SwiftUI, los Property Wrappers se convirtieron en la base de la gestión del estado de las vistas: @State, @Binding, @ObservedObject, @StateObject y @Environment. Según la documentación de Swift (2025), los envoltorios de propiedades reducen el código repetitivo en los proyectos en un promedio del 40%. Comprender Property Wrapper es esencial para todo desarrollador iOS que quiera trabajar eficazmente con el framework.
Puntos clave
Property Wrapper es una construcción del lenguaje Swift, introducida en la versión 5.1, que permite encapsular la lógica de acceso a propiedades en un tipo separado. En lugar de escribir getters y setters repetitivos en cada clase, el desarrollador declara el envoltorio una vez y lo aplica mediante la anotación @ antes del tipo. Swift envuelve automáticamente la propiedad en el tipo especificado, llamando a sus métodos wrappedValue y projectedValue al leer y escribir. Según Apple (WWDC 2019), los Property Wrappers se convirtieron en una abstracción clave para SwiftUI.
Un envoltorio de propiedad es una estructura o clase con el atributo @propertyWrapper. Internamente, dicho tipo debe implementar la propiedad wrappedValue, que devuelve y establece el valor real. El compilador de Swift reemplaza los accesos a la propiedad original por llamadas a wrappedValue, ocultando completamente la implementación del código llamante. Adicionalmente, se puede definir projectedValue — una proyección accesible mediante el símbolo $.
La ventaja de los Property Wrappers reside en la reutilización de la lógica. Por ejemplo, se puede crear un envoltorio para validación de email, almacenamiento en caché de valores o sincronización con almacenamiento — y aplicarlo a cualquier propiedad del proyecto. En SwiftUI, este concepto se utiliza en todas partes: cada mecanismo de gestión de estado se implementa como un Property Wrapper separado.
Al declarar una propiedad con la anotación @WrapperType var value: T, el compilador de Swift transforma el código. Crea una instancia de WrapperType y genera el acceso a la propiedad a través de wrappedValue. El código fuente let x = value se convierte en let x = _value.wrappedValue, y value = newValue en _value.wrappedValue = newValue. Esta transformación ocurre en tiempo de compilación, sin sobrecarga en tiempo de ejecución.
@propertyWrapper
struct Capitalized {
private var text: String
var wrappedValue: String {
get { text }
set { text = newValue.capitalized }
}
init(initialValue: String) {
text = initialValue.capitalized
}
}
El listado muestra el envoltorio Capitalized, que convierte automáticamente una cadena a formato con mayúscula inicial. Al asignar un valor, el setter llama a capitalized antes de guardarlo. Ahora cualquier propiedad con la anotación @Capitalized almacenará solo texto con el formato correcto. Este enfoque elimina por completo la duplicación de código de validación y formato.
La proyección (projectedValue) es un canal de comunicación adicional accesible mediante el prefijo $. En SwiftUI, esta funcionalidad se utiliza en todas partes: $state proporciona Binding
SwiftUI incluye cinco Property Wrappers incorporados para la gestión del estado: @State, @Binding, @ObservedObject, @StateObject y @Environment. Cada uno resuelve una tarea específica y se utiliza en diferentes escenarios. @State está diseñado para datos locales simples, @Binding — para pasar una referencia a datos a vistas hijas, @ObservedObject y @StateObject — para objetos complejos, @Environment — para valores del sistema de la jerarquía.
| Wrapper | Propósito | Posesión |
|---|---|---|
| @State | Estado local de una sola vista | Vista actual |
| @Binding | Conexión bidireccional con el padre | Vista padre |
| @ObservedObject | Observación de un objeto externo | Propietario externo |
| @StateObject | Creación de ObservableObject | Vista actual |
| @Environment | Valores del sistema de la jerarquía | Entorno SwiftUI |
La elección de un Property Wrapper específico depende de la fuente de datos y su ciclo de vida. Si los datos pertenecen a una sola vista y no son necesarios para componentes hijos — use @State. Si una vista hija necesita modificar los datos del padre — use @Binding. Para objetos utilizados en múltiples vistas, son adecuados @ObservedObject y @StateObject.
@State es un Property Wrapper para almacenar el estado local dentro de una sola vista. SwiftUI gestiona automáticamente la memoria para las propiedades @State y redibuja la vista en cada cambio. @State es adecuado para tipos simples (String, Int, Bool, enum) y estructuras que pertenecen exclusivamente a la vista actual. Cuando el valor cambia, SwiftUI vuelve a ejecutar la propiedad body.
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("Recuento: \(count)")
Button("Incrementar") {
count += 1
}
}
}
}
En el ejemplo, la propiedad @State count almacena el valor actual del contador. SwiftUI crea un área de almacenamiento para esta propiedad en el heap y la vincula al ciclo de vida de CounterView. Al pulsar el botón, count aumenta en 1, SwiftUI detecta el cambio y vuelve a ejecutar body, mostrando el nuevo valor. Importante: @State no debe usarse para tipos de referencia complejos — para eso están @StateObject y @ObservedObject.
@Binding crea una referencia a una fuente de datos que pertenece a otra vista. Binding no almacena un valor por sí mismo — lee y escribe datos a través de @State, @StateObject u otro Binding pasado desde el padre. Esto permite que los componentes hijos modifiquen el estado del ancestro sin poseer los datos directamente y sin callbacks.
struct ToggleSwitch: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
En el listado, ToggleSwitch recibe @BindingBool de la vista padre. El padre crea @State var isToggleOn = false y pasa $isToggleOn al inicializador de ToggleSwitch. Cuando el usuario cambia el interruptor dentro de la vista hija, el cambio se refleja inmediatamente en el @State del padre. El mecanismo Binding elimina por completo la necesidad de delegados o closures para pasar cambios hacia arriba en la jerarquía.
@ObservedObject es un Property Wrapper para observar una instancia de ObservableObject pasada desde fuera. La vista no posee este objeto — se crea en el componente padre o se inyecta a través de Environment. Cuando cualquier propiedad @Published dentro del ObservableObject cambia, SwiftUI redibuja todas las vistas suscritas mediante @ObservedObject.
@StateObject — un envoltorio para crear y poseer un ObservableObject directamente en la vista. A diferencia de @ObservedObject, @StateObject garantiza una única instancia del objeto durante todo el ciclo de vida de la vista. Incluso si SwiftUI recrea la estructura de la vista (lo que ocurre con frecuencia), @StateObject conserva el objeto existente y no vuelve a llamar al inicializador.
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
}
struct ProfileView: View {
@StateObject var settings = UserSettings()
var body: some View {
ChildProfileView(settings: settings)
}
}
struct ChildProfileView: View {
@ObservedObject var settings: UserSettings
var body: some View {
Text("Hola, \(settings.username)")
}
}
En el ejemplo, ProfileView crea UserSettings mediante @StateObject, convirtiéndose en el propietario del objeto. ChildProfileView recibe la misma instancia mediante @ObservedObject — observa pero no gestiona el ciclo de vida. Cuando username cambia, ambas vistas se actualizan. Si ChildProfileView usara @StateObject en lugar de @ObservedObject, en cada renderizado se crearía una nueva instancia con el valor inicial.
La regla clave: @StateObject se utiliza en la vista que crea el objeto (fuente de verdad), mientras que @ObservedObject se usa en la vista que recibe un objeto ya creado del padre. Violar esta regla conduce a la pérdida de estado o a recreaciones inesperadas de datos.
Swift permite crear Property Wrappers personalizados para cualquier lógica repetitiva de acceso a propiedades. Simplemente declare una estructura o clase con el atributo @propertyWrapper e implemente wrappedValue. A continuación se muestra el envoltorio UserDefaultsWrapper, que sincroniza automáticamente el valor con UserDefaults.
@propertyWrapper
struct UserDefaultsWrapper<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
struct AppConfig {
@UserDefaultsWrapper(key: "theme", defaultValue: "light")
var theme: String
}
El envoltorio UserDefaultsWrapper utiliza un genérico T para funcionar con cualquier tipo de datos compatible con UserDefaults. El getter lee el valor por clave, el setter lo escribe. Aplicar @UserDefaultsWrapper(key:defaultValue:) a la propiedad theme la vincula automáticamente al almacenamiento — toda la lógica de UserDefaults queda oculta dentro del envoltorio. Este es un ejemplo típico de reducción de código repetitivo con Property Wrappers.
Al crear envoltorios personalizados, es importante considerar el rendimiento. Dado que el getter y el setter se llaman cada vez que se accede a la propiedad, no se deben colocar operaciones pesadas de E/S en wrappedValue. Para almacenamiento de datos asíncrono, es mejor combinar Property Wrappers con ObservableObject y @Published.
Preguntas frecuentes
@State está diseñado para tipos simples (String, Int, Bool) y estructuras, mientras que @StateObject es para tipos de referencia que implementan ObservableObject. @State almacena el valor directamente en SwiftUI, @StateObject gestiona una instancia de clase en el heap.
Sí, @Binding se puede crear a partir de @StateObject, @ObservedObject o de otro Binding mediante la proyección $. También se puede inicializar Binding desde ObservableObject a través de $object.$publishedProperty o desde InlineBinding mediante Binding.constant(value).
Para datos globales, use @EnvironmentObject o inyecte ObservableObject a través de EnvironmentValues. @StateObject es adecuado para la vista raíz con posterior transmisión mediante @ObservedObject a los componentes hijos.
@ObservedObject no posee el objeto — si la vista padre se recrea y pasa una nueva instancia, @ObservedObject cambiará a ella. Para evitar la pérdida de estado, la vista propietaria debe usar @StateObject.
Sí, pero es más fácil usar una combinación de ObservableObject con @Published y funciones asíncronas dentro de la clase. Property Wrapper es síncrono por naturaleza — wrappedValue se calcula en cada acceso, lo que no es adecuado para operaciones de larga duración.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también