El uso de los property wrappers

0
2551
  1. Introducción
  2. Entorno
  3. Como crear tu propio property wrapper
  4. @State y @Binding: las claves de SwiftUI
  5. Como usar @EnvironmentObject y @Environment
  6. Como usar @Published y @ObservedObject
  7. Conclusiones
  8. Referencias

Introducción

Con la introducción de Swift 5.1 ahora tenemos el property wrapper: una cosa fundamental en SwiftUI. Un property wrapper añade una capa de separación entre el código que administra cómo se almacena una propiedad y el código que define una propiedad. Por ejemplo, si tiene propiedades que proporcionan comprobaciones de seguridad de procesos o almacenan sus datos en una base de datos, debes escribir este código en cada propiedad. Cuando se usa un property wrapper, tú escribes el código de administración una vez cuando defines el property wrapper, y luego reutilizas este código de administración aplicándolo a múltiples propiedades. Hay patrones de implementación de propiedades que surgen repetidamente. Por eso han introducido un mecanismo general de «wrapper de properties» para permitir que estos patrones se definan solo una vez.

Entorno

Este tutorial está escrito usando el siguiente entorno:

    • Hardware: MacBook Pro 15’ (2,5 GHz Intel Core i7, 16GB DDR3, mediados de 2015)
    • Sistema operativo: macOS Catalina 10.15
    • Versiones del software:
      • Xcode: 11
      • iOS SDK: 13.4

Es importante saber que para probar SwiftUI se necesita instalar Mac OS Catalina 10.15 y Xcode 11.

Cómo crear tu propio property wrapper

El siguiente código muestra un patrón que podrás reconocer. Se crea un enumerado de los claves dentro de la extension de la clase UserDefaults para hacer que las propiedades sean accesibles sin tener que pegar las claves de String en todo el proyecto.

extension UserDefaults {
/// Indicates whether or not the user has seen the introduction. 
       var hasSeenIntroduction: Bool { 
         set { set(newValue, forKey: Keys.hasSeenIntroduction) } 
         get { return bool(forKey: Keys.hasSeenIntroduction) } }
       }
       public enum Keys {
         static let hasSeenIntroduction = "has_seen_introduction"
    }
}

Se permite establecer y obtener valores de la siguiente manera:

UserDefaults.standard.hasSeenIntroduction = true
guard !UserDefaults.standard.hasSeenIntroduction else { return }
showIntroduction()

Ahora, como parece ser una buena solución, fácilmente podría terminar siendo un archivo grande con muchas claves y propiedades definidas. Pero existe una manera de facilitar el código aún mas. Con el property wrapper. Solo tenemos que añadir la palabra clave @propertyWrapper y tener una propiedad wrappedValue (es obligatorio) para convertirlo en el property wrapper.

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    init(_ key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }
    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

El wrapper permite pasar un valor predeterminado por si aún no hay un valor registrado. Podemos pasar cualquier tipo de valor ya que el contenedor se define con un valor genérico T. Lamentablemente, no podemos usar el property wrapper en una extensión en UserDefaults. En cambio, tenemos que crear un nuevo objeto contenedor que llamamos UserDefaultsConfig en este ejemplo. La razón de esto es que las propiedades almacenadas de clase (stored property) no son compatibles con las extensiones de clase.

El nuevo contenedor se puede definir de la siguiente manera:

struct UserDefaultsConfig {
    @UserDefault("has_seen_introduction", defaultValue: false)
    static var hasSeenIntroduction: Bool
}

Como se puede ver, podemos usar el inicializador del property wrapper definido. Pasamos la misma clave que usamos antes y establecemos el valor predeterminado en falso. Usar esta nueva propiedad es simple:

UserDefaultsConfig.hasSeenIntroduction = false
print(UserDefaultsConfig.hasSeenIntroduction) // Prints: false
UserDefaultsConfig.hasSeenIntroduction = true
print(UserDefaultsConfig.hasSeenIntroduction) // Prints: true

Así podemos crear nuestros propios property wrappers, pero SwiftUI nos proporciona ya diferentes property wrappers.

Además de un valor envuelto, un property wrapper puede también proporcionar un valor adicional (projectedValue); por ejemplo, un property wrapper que administra el acceso a una base de datos puede exponer el estado de conexión a la base de datos. El nombre del valor proyectado es el mismo que el valor encubierto (wrappedValue), excepto que comienza con un signo de dólar ($). Debido a que en el código no se puede definir propiedades que comiencen con $, el valor proyectado nunca interfiere con las propiedades que tú defines.

@State y @Binding: las claves de SwiftUI

SwiftUI usa los property wrappers extensamente. Los más importantes son @State y @Binding. Normalmente se marcan con @State un valor persistente de un tipo dado, a través del cual una vista lee y observa el valor. SwiftUI administra el almacenamiento de cualquier propiedad que declare como @State. Cuando el valor de esta propiedad cambia, la vista invalida su apariencia y vuelve a calcular el cuerpo (el método body). Usa la propiedad @State como la única fuente de verdad para una vista dada.
Una propiedad @State no es el valor en sí mismo. Es un medio de leer y mutar el valor. Solo acceda a una propiedad @State desde el interior del cuerpo (body) de la vista (o desde funciones llamadas por ella). Por esta razón, tienes que declarar sus propiedades @State como privadas, para evitar que los clientes de su vista accedan a ellas.
Se puede obtener un enlace (Binding) de State a través de su propiedad ‘binding’, o utilizando el operador de prefijo ‘$’.

Y ¿qué es un binding? Binding es un enlace de un valor que proporciona una forma de mutarlo. Se usa para crear una conexión bidireccional entre una vista y su modelo. Por ejemplo, se puede crear un enlace entre un componente Toggle y una propiedad booleana @State. La interacción con el componente cambia el valor de Bool, y la mutación del valor de Bool hace que el componente actualice su estado.

Ojo!: las propiedades de @State siempre deben estar relacionadas con una vista en SwiftUI. ¡Así que asegúrate de declararlas siempre dentro de una estructura de View (pero no dentro del body de la vista)! 

@State

Entonces, ¿qué hace una propiedad del @State? Bueno, puede leer y manipular datos tal como lo hace con variables regulares en Swift. Pero la diferencia clave es que cada vez que los datos de un @State cambian, la vista relacionada se reconstruye.

struct ContentView: View {
    
    @State var myInteger = 1
    
    var body: some View {
        VStack {
            Text("\(myInteger)")
            Button(action: {self.myInteger += 1}) {
                Text("Tap me!")
            }
        }
    }

Tenemos una variable entera envuelta en un @State. En la vista relacionada, tenemos un objeto de texto que lee los datos del @State y un botón. Cuando tocamos el botón, el valor del @State aumenta. Como hemos aprendido, esta manipulación hace que toda la vista vuelva a pintarse y que el componente Text finalmente muestre el valor actualizado.

Las propiedades de @State siempre deben estar relacionadas con una vista específica. Pero a veces queremos tener acceso a una propiedad del @State desde el exterior, por ejemplo, desde las vistas secundarias.

@Binding

Para crear dicha referencia, usamos el property wrapper @Binding. Por ejemplo, si queremos crear un enlace desde una vista secundaria a la vista que contiene la propiedad State, simplemente declara una variable dentro de la vista secundaria y márcala con la palabra clave @Binding.

struct ChildView: View {
    
    @Binding var myBinding: String
    
    var body: some View {
        //...
    }
}

Después puedes crear el enlace al @State inicializando la vista secundaria con referencia al @State utilizando la sintaxis «$».

struct ContentView: View {
    
    @State var myBinding = "Hello"
    
    var body: some View {
         ChildView(myBinding: $myBinding)
    }
    
}

Y luego puedes usar el enlace como lo haces con las propiedades de @State. Por ejemplo, cuando actualiza la propiedad @Binding, también hace que la propiedad @State cambie sus datos, lo que hace que la vista también vuelva a pintarse.

struct ContentView: View {
    
    @State var myInteger = 1
    
    var body: some View {
        VStack {
            Text("\(myInteger)")
            OutsourcedButtonView(myInteger: $myInteger)
        }
    }
    
}

struct OutsourcedButtonView: View {
    
    @Binding var myInteger: Int
    
    var body: some View {
        Button(action: {self.myInteger += 1}) {
            Text("Tap me!")
        }
    }
}

En el ejemplo anterior, ContentView contiene una propiedad de @State que contiene un número entero y un texto para mostrar esos datos. OutsourcedButtonView contiene un enlace a ese estado y un botón para aumentar el valor de la propiedad del enlace. Cuando tocamos el botón, los datos del @State se actualizan a través del enlace, lo que hace que ContentView se vuelva a mostrar y finalmente muestre el nuevo entero.

Como usar @EnvironmentObject y @Environment

SwiftUI nos proporciona los property wrappers @Environment y @EnvironmentObject, pero son sutilmente diferentes: mientras que @EnvironmentObject nos permite inyectar valores arbitrarios en el entorno, @Environment está específicamente allí para trabajar con claves predefinidas.

Por ejemplo, @Environment es excelente para leer cosas como el contexto de un objeto gestionado de Core Data, ya sea que el dispositivo esté en modo oscuro o claro, con qué clase de tamaño se visualiza su vista y más: propiedades fijas que provienen del sistema . En código, se ve así:

@Environment (\.horizontalSizeClass) var horizontalSizeClass
@Environment (\.managedObjectContext) var managedObjectContext

Por otro lado, @EnvironmentObject está diseñado para que se lean objetos arbitrarios del entorno, de esta manera:

@EnvironmentObject var order: Order

Esa diferencia puede sonar pequeña, pero es importante debido a la forma en que se implementa @EnvironmentObject. Cuando decimos que el objeto orden es de tipo Orden SwiftUI buscará en su entorno para encontrar un objeto de ese tipo y adjuntarlo a la propiedad de orden. Sin embargo, cuando se usa @Environment, el mismo comportamiento no es posible, porque muchas cosas pueden compartir el mismo tipo de datos. Por ejemplo:

@Environment (\.accessibilityReduceMotion) var reduceMotion
@Environment (\.accessibilityReduceTransparency) var reduceTransparency
@Environment (\.accessibilityEnabled) var accessibilityEnabled

Las tres claves de entorno devuelven un booleano, por lo que sin especificar exactamente a qué clave nos referimos sería imposible leerlas correctamente.

@EnvironmentObject

Es un property wrapper para crear un modelo de datos que, una vez inicializado, puede compartir datos con todas las vistas de su aplicación. El uso de @EnvironmentObjects es adecuado para pasar datos a varias vistas a la vez sin necesidad de crear una «cadena de inicialización». Pero hay que tener en cuenta que se necesita crear un objeto ViewModel que implemente la interfaz ObservableObject y que sea nuestro modelo.

class ViewModel: ObservableObject {
    @Published var showingAlert = false
    @Published var name = "Anton"
}

Este objeto puede ser muy simple, pero lo importante es que sea una clase e implemente la interfaz ObservableObject. Cada property puede ser @Published para que Swift propague sus actualizaciones. O podemos implementar el protocolo ObservableObject manualmente.

class MyObservableObject: ObservableObject {
    
    let objectWillChange = PassthroughSubject<MyObservableObject,Never>()

    var myInteger = 1 {
        didSet {
            objectWillChange.send(self)
        }
    }
    func increaseInteger() {
        myInteger += 1
    }
}

Es importante también crear ViewModel en el delegado de escena (Scene Delegate).

//create ViewModel 
var viewModel = ViewModel()

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let contentView = ContentView()
        
        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView.environmentObject(viewModel))
            self.window = window
            window.makeKeyAndVisible()
        }
    }

Luego dentro de cada clase para utilizar nuestro modelo simplemente basta con declararlo con el property wrapper @EnvironmentObject. ¡Es así de fácil!

struct ContentView : View {
    @EnvironmentObject var viewModel: ViewModel
    
    var body: some View {
        NavigationView {
                VStack(spacing: 20.0) {
                    ChildView()
                    NavigationLink(destination: DetailView()) {
                        Text("Go to Detail")
                    }
                }
            }.padding(.all, 10.0)
            .navigationBarTitle("Main Page")
        }
}

struct ChildView: View {
    @EnvironmentObject var viewModel: ViewModel

    var body: some View {
        VStack {
            TextField("Placeholder", text: $viewModel.name)
                .padding(.horizontal, 15.0)
            Button(action: {
                self.viewModel.showingAlert = true
            }) {
                Text("Show Alert")
                    .foregroundColor(Color.red)
            }
        }.alert(isPresented: $viewModel.showingAlert) {
        Alert(title: Text("Important message"), 
              message: Text("Welcome \(viewModel.name)"), 
              dismissButton: .default(Text("Got it!")))
            }
    }
}

De esta manera, no tenemos que pasar nuestro modelo por todas las clases de nuestra jerarquía. @EnvironmentObject nos facilita el trabajo y ayuda tener solo «una fuente de  la verdad», evitando crear las variables-copias en cada clase.

Como usar @Published y @ObservedObject

Los objetos observables son similares a las propiedades de @State que ya conoce. Pero en lugar de representar una vista en función de sus datos asignados, ObservableObjects son capaces de hacer lo siguiente:

  • observableObjects son clases, no variables
  • estas clases pueden contener datos, por ejemplo, una variable de tipo String
  • podemos vincular una o múltiples vistas al Objeto Observable (o mejor dicho, podemos hacer que estas vistas observen el objeto)
  • las vistas pueden acceder y manipular los datos dentro del ObservableObject. Cuando se produce un cambio en los datos del ObservableObject, todas las vistas se vuelven a pintarse automáticamente, de forma similar a cuando una propiedad @State cambia

Aquí hay un ejemplo sobre cómo implementar un ObservableObject:

import SwiftUI
import Combine

struct ContentView: View {
    
    @ObservedObject var myObservedObject: MyObservableObject
    
    var body: some View {
        VStack {
            Text("\(myObservedObject.myInteger)")
            Button(action: {self.myObservedObject.increaseInteger()}) {
                Text("Tap me!")
            }
        }
    }
    
}

class MyObservableObject: ObservableObject {
    
    let objectWillChange = PassthroughSubject<MyObservableObject,Never>()

    var myInteger = 1 {
        didSet {
            objectWillChange.send(self)
        }
    }
    
    func increaseInteger() {
        myInteger += 1
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(myObservedObject: MyObservableObject())
    }
}
#endif

Asegúrate de importar Combine Framework y cumplir con el protocolo @ObservableObject. Podemos hacer que una vista observe un ObservableObject creando una propiedad @ObservedObject. La vista puede acceder al ObservableObject y también manipular sus datos. En el ejemplo anterior, la vista llama a la función aumentarInteger de ObservableObject que aumenta el valor de la variable myInteger dentro de MyObservableObject. Esta manipulación hace que ContentView se vuelva a repintarse y finalmente muestre el valor actualizado.

Conclusiones

Los property wrappers son una potente novedad de Swift 5, que agrega una capa de separación entre el código que gestiona cómo se almacena una propiedad y el código que define una propiedad.

Cuando decidas utilizar los property wrappers, asegúrate de tener en cuenta sus inconvenientes:

  • Los property wrappers aún no son compatibles con el código de nivel superior (de momento con el Swift 5.2). No se puede definirlos fuera de las clases/estructuras/etc.
  • Una propiedad con un property wrapper no puede ser redifinida en una subclase.
  • no puede ser «lazy», @NSCopying, @NSManaged, @weak o @unowned.
  • no puede tener un método set o get.
  • wrapValue, init (wrapValue) y projectedValue deben tener el mismo nivel de control de acceso que el tipo de contenedor.
  • Una propiedad con un property wrapper no puede declararse en un protocolo o una extensión.
  • Los property wrappers requieren Swift 5.1, Xcode 11 e iOS 13.
  • Los property wrappers agregan aún más azúcar sintáctica a Swift, lo que dificulta su comprensión y aumenta la barrera de entrada para los recién llegados.

A pesar de todos los inconvenientes es un mecanismo muy potente y eficaz, pero es imprescindible en el desarrollo para SwiftUI.

Referencias

  1. Property wrapper
  2. https://nshipster.com/propertywrapper/
  3. https://www.raywenderlich.com/3715234-swiftui-getting-started
  4. Swift Evolution: Property wrapper proposal

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad