Si usó SwiftUI, probablemente prestó atención a palabras clave como @ObservedObject, @EnvironmentObject, @FetchRequest, etc. Los envoltorios de propiedades (en adelante denominados "envoltorios de propiedades") son una nueva característica de Swift 5.1. Este artículo lo ayudará a comprender de dónde provienen todas las construcciones de @, cómo usarlas en SwiftUI y en sus proyectos.

Traducido por: Evgeny Zavozhansky, desarrollador de FunCorp.
Nota: Cuando se preparó la traducción, parte del código fuente del artículo original había perdido su relevancia debido a cambios en el idioma, por lo que algunos ejemplos de código fueron reemplazados intencionalmente.
Los envoltorios de propiedades se introdujeron por primera vez en los foros de Swift en marzo de 2019, unos meses antes del anuncio de SwiftUI. En su propuesta original, Douglas Gregor, miembro del equipo de Swift Core, describió esta construcción (luego llamada delegados de propiedad) como "una generalización accesible para el usuario de la funcionalidad que actualmente proporciona una construcción de lenguaje como lazy
, por ejemplo".
Si una propiedad se declara con la palabra clave lazy
, esto significa que se inicializará la primera vez que se acceda a ella. Por ejemplo, la inicialización de propiedad diferida podría implementarse utilizando una propiedad privada, a la que se accede a través de una propiedad calculada. Pero usar la palabra clave lazy
hace mucho más fácil.
struct Structure {
SE-0258: Property Wrapper explica perfectamente el diseño y la implementación de los wrappers de propiedades. Por lo tanto, en lugar de tratar de mejorar la descripción en la documentación oficial, considere algunos ejemplos que se pueden implementar utilizando envoltorios de propiedades:
- restricción de valores de propiedad;
- conversión de valores al cambiar propiedades;
- cambio de semántica de igualdad y comparación de propiedades;
- Registro de acceso a la propiedad.
Límite de valores de propiedad
SE-0258: Property Wrapper proporciona varios ejemplos prácticos, incluidos @Clamping
, @Copying
, @Atomic
, @ThreadSpecific
, @Box
, @UserDefault
. Considere el contenedor @Clamping
, que le permite limitar el valor máximo o mínimo de una propiedad.
@propertyWrapper struct Clamping<Value: Comparable> { var value: Value let range: ClosedRange<Value> init(initialValue value: Value, _ range: ClosedRange<Value>) { precondition(range.contains(value)) self.value = value self.range = range } var wrappedValue: Value { get { value } set { value = min(max(range.lowerBound, newValue), range.upperBound) } } }
@Clamping
se puede usar, por ejemplo, para simular la acidez de una solución, cuyo valor puede tomar un valor de 0 a 14.
struct Solution { @Clamping(0...14) var pH: Double = 7.0 } let carbonicAcid = Solution(pH: 4.68)
Intentar establecer el valor de pH más allá del rango de (0...14)
hará que la propiedad tome el valor más cercano al intervalo mínimo o máximo.
let superDuperAcid = Solution(pH: -1) superDuperAcid.pH
Los contenedores de propiedades se pueden usar para implementar otros contenedores de propiedades. Por ejemplo, el contenedor @UnitInterval
limita el valor de una propiedad al intervalo (0...1)
usando @Clamping(0...1)
:
@propertyWrapper struct UnitInterval<Value: FloatingPoint> { @Clamping(0...1) var wrappedValue: Value = .zero init(initialValue value: Value) { self.wrappedValue = value } }
Ideas similares
@Positive
/ @NonNegative
indica que el valor puede ser un número positivo o negativo.@NonZero
indica que el valor de la propiedad no puede ser 0.@Validated
o @Whitelisted
/ @Blacklisted
restringe el valor de una propiedad a ciertos valores.
Convertir valores al cambiar propiedades
Validar los valores de los campos de texto es un dolor de cabeza constante para los desarrolladores de aplicaciones. Hay tantas cosas a tener en cuenta: desde lugares comunes como la codificación hasta intentos maliciosos de ingresar un código a través de un campo de texto. Considere usar un contenedor de propiedades para eliminar los espacios que un usuario ha ingresado al principio y al final de una línea.
import Foundation let url = URL(string: " https:
Foundation
ofrece el método trimmingCharacters(in:)
, con el que puede eliminar espacios al principio y al final de una línea. Puede llamar a este método siempre que necesite garantizar la exactitud de la entrada, pero no es muy conveniente. Puede usar el contenedor de propiedades para esto.
import Foundation @propertyWrapper struct Trimmed { private(set) var value: String = "" var wrappedValue: String { get { return value } set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } } init(initialValue: String) { self.wrappedValue = initialValue } }
struct Post { @Trimmed var title: String @Trimmed var body: String } let quine = Post(title: " Swift Property Wrappers ", body: "…") quine.title
Ideas similares
@Transformed
aplica la conversión de ICU a la cadena de @Transformed
.@Rounded
/ @Truncated
redondea o trunca un valor de cadena.
Cambiar la semántica de igualdad y comparación de propiedades
En Swift, dos cadenas son iguales si son canónicamente equivalentes , es decir Contienen los mismos caracteres. Pero supongamos que queremos que las propiedades de cadena sean iguales, no sensibles a mayúsculas y minúsculas, que contienen.
@CaseInsensitive
implementa un contenedor para propiedades de tipo String
o SubString
.
import Foundation @propertyWrapper struct CaseInsensitive<Value: StringProtocol> { var wrappedValue: Value } extension CaseInsensitive: Comparable { private func compare(_ other: CaseInsensitive) -> ComparisonResult { wrappedValue.caseInsensitiveCompare(other.wrappedValue) } static func == (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedSame } static func < (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedAscending } static func > (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedDescending } }
let hello: String = "hello" let HELLO: String = "HELLO" hello == HELLO
Ideas similares
@Approximate
para una comparación aproximada de propiedades de tipo Double o Float.@Ranked
por propiedades cuyos valores están en orden (por ejemplo, el rango de las cartas).
Registro de acceso a la propiedad
@Versioned
le permitirá interceptar los valores asignados y recordar cuándo se configuraron.
import Foundation @propertyWrapper struct Versioned<Value> { private var value: Value private(set) var timestampedValues: [(Date, Value)] = [] var wrappedValue: Value { get { value } set { defer { timestampedValues.append((Date(), value)) } value = newValue } } init(initialValue value: Value) { self.wrappedValue = value } }
La clase ExpenseReport
permite guardar marcas de tiempo de estados de procesamiento del informe de gastos.
class ExpenseReport { enum State { case submitted, received, approved, denied } @Versioned var state: State = .submitted }
Pero el ejemplo anterior demuestra una seria limitación en la implementación actual de envoltorios de propiedades, que se deriva de la restricción Swift: las propiedades no pueden arrojar excepciones. Si quisiéramos agregar una restricción a @Versioned
para evitar que el valor cambie a .approved
después de que tomó el valor .denied
, entonces la mejor opción es fatalError()
, que no es adecuada para aplicaciones reales.
class ExpenseReport { @Versioned var state: State = .submitted { willSet { if newValue == .approved, $state.timestampedValues.map { $0.1 }.contains(.denied) { fatalError("") } } } } var tripExpenses = ExpenseReport() tripExpenses.state = .denied tripExpenses.state = .approved
Ideas similares
@Audited
para @Audited
acceso a la propiedad.@UserDefault
para encapsular el mecanismo para leer y guardar datos en UserDefaults
.
Limitaciones
Las propiedades no pueden lanzar excepciones
Como ya se mencionó, los envoltorios de propiedades pueden usar solo unos pocos métodos para procesar valores no válidos:
- ignóralos;
- terminar la aplicación usando fatalError ().
Las propiedades envueltas no se pueden marcar con el atributo `typealias`
El ejemplo @UnitInterval
anterior, cuya propiedad está limitada por el intervalo (0...1)
, no se puede declarar como
typealias UnitInterval = Clamping(0...1)
Restricción en el uso de una composición de varios envoltorios de propiedades
La composición de envoltorios de propiedades no es una operación conmutativa: el orden de la declaración afectará el comportamiento. Considere un ejemplo en el que la propiedad slug, que es la url de una publicación de blog, está normalizada. En este caso, el resultado de la normalización variará dependiendo de cuándo se reemplacen los espacios por guiones, antes o después de la eliminación de espacios. Por lo tanto, por el momento, no se admite una composición de varios envoltorios de propiedades.
@propertyWrapper struct Dasherized { private(set) var value: String = "" var wrappedValue: String { get { value } set { value = newValue.replacingOccurrences(of: " ", with: "-") } } init(initialValue: String) { self.wrappedValue = initialValue } } struct Post { … @Dasherized @Trimmed var slug: String
Sin embargo, esta limitación se puede eludir mediante el uso de envoltorios de propiedades anidadas.
@propertyWrapper struct TrimmedAndDasherized { @Dasherized private(set) var value: String = "" var wrappedValue: String { get { value } set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } } init(initialValue: String) { self.wrappedValue = initialValue } } struct Post { … @TrimmedAndDasherized var slug: String }
Otras restricciones de envoltorio de propiedad
- No se puede usar dentro del protocolo.
- Una instancia de propiedad de contenedor no se puede declarar en
enum
. - Una propiedad envuelta declarada dentro de una clase no puede ser anulada por otra propiedad.
- Una propiedad envuelta no puede ser
lazy
, @NSCopying
, @NSManaged
, weak
o unowned
propiedad. - Una propiedad envuelta debe ser la única dentro de su definición (es decir,
@Lazy var (x, y) = /* ... */
). - Una propiedad envuelta no puede tener
getter
y setter
definidos. - Los tipos de la propiedad
wrappedValue
y la variable wrappedValue
en init(wrappedValue:)
deben tener el mismo nivel de acceso que el tipo de contenedor de la propiedad. - La propiedad type de
projectedValue
debe tener el mismo nivel de acceso que el tipo wrapper de la propiedad. init()
debe tener el mismo nivel de acceso que el tipo de contenedor de propiedades.
Resumamos Los contenedores de propiedades en Swift proporcionan a los autores de la biblioteca acceso al comportamiento de alto nivel previamente reservado para las funciones del lenguaje. Su potencial para mejorar la legibilidad y reducir la complejidad del código es enorme, y solo examinamos superficialmente las capacidades de esta herramienta.
¿Utiliza envoltorios de propiedades en sus proyectos? Escribe en los comentarios!