Introducidos en Swift 5.1, los contenedores de propiedades se convirtieron rápidamente en una de las características más utilizadas en todo lo relacionado con Swift. El ahora famoso marco SwiftUI probablemente no podría haber sido como es ahora sin sus @State
, @Binding
, @StateObject
y otras piezas de magia negra que utilizas todos los días. O tal vez escuchaste o incluso usaste uno de los marcos de mapeadores relacionales de objetos (ORM) más populares para un Swift del lado del servidor llamado Fluent . No te dejan otra opción que averiguar cómo y cuándo usar un signo de dólar $
antes de una propiedad cuidadosamente envuelta en otro contenedor.
La verdad es que los contenedores de propiedades no son tan difíciles de descifrar cuando profundizas y tratas de usarlos por tu cuenta. De hecho, es probable que tengas un montón de lugares en tu proyecto donde puedas hacer las cosas más fáciles si incorporas un contenedor como ayuda. En esta guía, cubriré 5 casos de uso básicos como ese. Pero primero, refresquemos nuestra memoria sobre el tema en sí.
Un contenedor de propiedades es un tipo anotado especialmente que encapsula una parte de la lógica aplicada a cualquier propiedad en general o propiedades restringidas a un tipo específico que usted elija. Puede ser una estructura, una enumeración o una clase que se declara con un atributo @propertyWrapper
y tiene al menos un wrappedValue
como propiedad obligatoria dentro:
@propertyWrapper struct SomeWrapper<T> { var wrappedValue: T { // mandatory property get { value } set { value = newValue } } private var value: T init(wrappedValue: T) { self.value = wrappedValue } }
Dentro de un captador y un definidor de la propiedad de valor encapsulado, puedes agregar cualquier lógica que necesites e interceptar, transformar o usar valores básicamente de cualquier manera que desees. Luego, puedes usar el encapsulador de esta manera:
class SomeService { @SomeWrapper fancyNumber: Int init(number: Int) { self._number = SomeWrapper(wrappedValue: number) // use "_" to initialize a property wrapper } func doSmth() { print(fancyNumber) } }
Lo bueno de los contenedores de propiedades es que puedes agregar tus propios inicializadores, métodos adicionales, propiedades, etc. que pueden tener cualquier funcionalidad que necesites. Una de ellas es una propiedad projectedValue
opcional que puede devolver incluso un contenedor en sí mismo o cualquier otro tipo. Ahora, recuerda: cuando quieras acceder a algo que pertenece al contenedor en sí, pero no a su valor, necesitas usar un famoso signo de dólar:
@propertyWrapper struct SomeWrapper<T> { var wrappedValue: T { get { value } set { value = newValue } } private var value: T init(wrappedValue: T) { self.value = wrappedValue } func methodToDoSmth() { ... } // extra wrapper method } class SomeService { ... func doSmth() { $fancyNumber.methodToDoSmth() // here we access the logic of the wrapper itself } }
Por supuesto, todos los ejemplos sin sentido anteriores no demuestran lo suficiente el verdadero poder de los envoltorios de propiedades, así que echemos un vistazo a algunas de las formas más útiles de hacerlos brillar.
Inspirados por todas las cosas que Fluent puede hacer, podemos aplicar contenedores de propiedades para manejar UserDefaults
en lugar de crear un servicio separado para ello de esta manera:
@propertyWrapper struct DefaultsStorage<T: Codable> { private let key: String private let defaultValue: T init(key: String, defaultValue: T) { self.key = key self.defaultValue = defaultValue } var wrappedValue: T { get { // getting and decodind the data from UserDefaults guard let data = UserDefaults.standard.object(forKey: key) as? Data, let value = try? JSONDecoder().decode(T.self, from: data) else { return defaultValue } return value } set { // encodind the data and saving to UserDefaults guard let encoded = try? JSONEncoder().encode(newValue) else { return } let defaults = UserDefaults.standard defaults.set(encoded, forKey: key) } } }
Como puedes ver, el contenedor anterior se puede usar con cualquier cosa Codable
@DefaultsStorage(key: "upload_date", defaultValue: Date()) var uploadDate: Date
La implementación anterior se puede modificar y cambiar mucho según sus necesidades. Por ejemplo, puede hacerla verdaderamente versátil al aceptar otros tipos o permitir el uso UserDefaults
basados en la suite en lugar del singleton standard
incluido.
Otra cosa es la información que guardas con él. Si quieres guardar algo de forma segura, es posible que quieras escribir una solución similar para Keychain.
Por cierto, Apple ya tiene un contenedor similar llamado @AppStorage
, pero fue diseñado para usarse con SwiftUI, lo cual quizás no quieras necesariamente.
En los casos en los que necesites monitorear un valor específico, el siguiente contenedor de propiedades puede serte útil:
@propertyWrapper struct Logged<Value> { private var value: Value private let label: String // label that will be used in the log event init(wrappedValue: Value, label: String) { self.value = wrappedValue self.label = label print("\(label): Initial value set to \(value)") } var wrappedValue: Value { get { value } set { // prints the set event to a console print("\(label): Value changed from \(value) to \(newValue)") value = newValue } } }
@Logged(label: "Total Price") var price = Double(12.5)
Este contenedor de propiedades puede resultar aún más útil si se le inyecta un servicio de análisis adicional. Puede ser algo local o incluso remoto. Si utiliza este último, recuerde filtrar en el sitio de llamada de dicho servicio para evitar llamadas innecesarias.
A menudo necesitamos validar la entrada del usuario. Hay muchos tipos de entrada posibles, como correo electrónico, número de teléfono, fecha de nacimiento, etc. Todos ellos requieren una pieza lógica específica aplicada a los datos originales. Puedes resolver esto usando un contenedor de propiedades para cada entrada:
@propertyWrapper struct ValidatedEmail<Value: StringProtocol> { var value: Value? var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } init(wrappedValue value: Value?) { self.value = value } private func validate(email: Value?) -> Bool { guard let email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }
O incluso puedes probar una solución única como ésta:
@propertyWrapper struct Validated<Value> { // define all the possible inputs enum InputType { case email case phoneNumber ... } let inputType: InputType var value: Value? var wrappedValue: Value? { get { // return value if validation succeeds or nil if not return validate(value) ? value : nil } set { value = newValue } } init(inputType: InputType, wrappedValue value: Value?) { self.inputType = inputType self.value = value } private func validate(_ value: Value?) -> Bool { // include validation logic for every input type switch inputType { case .email: ... case .phoneNumber: ... } } }
De manera similar a la validación de cosas, puedes formatear fechas con envoltorios de propiedades definiendo un conjunto de formatos prefijados que usarías dentro del envoltorio.
A veces, es necesario limitar un valor a un rango determinado, por ejemplo, un porcentaje que va de 0 a 100 o un valor fraccionario de 0 a 1, etc. También puede encapsular esta lógica en un contenedor de propiedades:
@propertyWrapper struct Clamped<Value: Comparable> { private var value: Value private let range: ClosedRange<Value> init(wrappedValue: Value, _ range: ClosedRange<Value>) { self.range = range self.value = min(max(wrappedValue, range.lowerBound), range.upperBound) } var wrappedValue: Value { get { value } set { value = min(max(newValue, range.lowerBound), range.upperBound) } } }
Puedes usarlo así:
struct ClampedExample { @Clamped(0...10) var number: Int init(number: Int) { self._number = Clamped(wrappedValue: number, 0...10) } func clampTest() { number = 20 print(example.number) // 10, because 20 is clamped to the upper bound 10 number = -5 print(example.number) // 0, because -5 is clamped to the lower bound 0 } }
Otro caso de uso de los contenedores de propiedades es ayudarnos con todo lo relacionado Codable
. La implementación de la lógica de codificación y decodificación personalizada se puede colocar de forma segura dentro de un contenedor si es necesario. También puede codificar automáticamente una cadena en base64 con esta sencilla solución:
@propertyWrapper struct Base64Encoding { private var value = "" var wrappedValue: String { get { Data(value.utf8).base64EncodedString() } set { value = newValue } } }
Al utilizar el contenedor anterior, siempre que acceda al texto transformado por él, devolverá una cadena codificada en base64.
Sin embargo, cuando se trabaja con contenedores de propiedades, no olvide que son los mismos tipos básicos de Swift que conocemos desde hace años, pero con una característica adicional añadida. Una de las muchas ideas populares sobre cómo usar un contenedor de propiedades en el pasado era intentar usar propiedades seguras para subprocesos con él. Puede encontrar múltiples implementaciones de @Atomic
en línea, pero resultó que la mayoría de ellas son bastante problemáticas . Aunque también se pueden encontrar soluciones para ello, ya no recomiendo usar @Atomic
, especialmente con los actores disponibles para cumplir este propósito.
Espero que hayas aprendido mucho o al menos hayas refrescado tus conocimientos sobre los contenedores de propiedades en Swift. Con este repaso, echa un vistazo a tus proyectos actuales: ¡quizás tengas algunas piezas que puedan aprovechar el poder de esta función!