Una de las principales preguntas que nos surge al construir una app con SwiftUI, el nuevo framework de UI de Apple, es cómo manejar un estado entre distintos flujos y vistas. Para ello Apple provee nuevas maneras de manejar los flujos de datos. Siendo este tema uno de los más abstractos al momento de abordar una app con SwiftUI.
Pero, ¿qué son los properties wrappers? Son esencialmente, un tipo de dato que envuelve a una variable para poder añadir más funcionalidad y lógica. Es una forma rápida de obtener la funcionalidad ya lista para su uso.
Para este tutorial: supongamos que estamos desarrollando un eCommerce mobile app. Para ello estamos siguiendo el patrón MVVM, Modelo Vista Vista-Modelo.
@State
Podemos usar este property wrapper para mantener un estado local a la vista. Por ejemplo, el estado enabled o disabled de un botón.
En el caso de nuestra app, el botón para realizar una compra lo podemos hacer persistir de la siguiente manera en nuestra interfaz de usuario.
struct BuyButton: View { @State var isDisabled: Bool = false
var body: some View { Button("Hacer la compra", action: {}) .disabled(isDisabled) } }
https://gist.github.com/Thonyvb/0147039825f003ee307143e427f0ab9f
Un cambio en @State var isDisabled produce que los elementos de interfaz como nuestro botón, reaccionen automáticamente si el valor cambia.
@Binding
Siguiendo con nuestro ejemplo, supongamos que tenemos una subvista que incluye un checkbox de favoritos.
Si deseamos que la subvista, FavButton, pueda interactuar con el estado de su vista padre ProductView podemos usar @Bindings para referenciarlos de la siguiente manera:
struct ProductView: View { @State private var isFavorite: Bool = false var body: some View { // ... FavButton(isFavorite: $isFavorite) // Pass a binding. } }
struct FavButton: View { @Binding var isFavorite: Bool var body: some View { Button(action: { isFavorite.toggle()
}, label: { Image(isFavorite ? "Filled_Heart" : "Heart") }) } }
https://gist.github.com/Thonyvb/69dff923fc54ed123404b16e1b7e4600
Es decir, nuestra subvista va a reaccionar al valor de @State private var isFavorite: Bool = false. El valor de isFavorite puede cambiar desde la sub vista FavBoton al recibir un click en el botón, y este valor ahora persiste en la vista padre ProductView.
Para los siguientes pasos en nuestra aplicación, necesitamos manejar información que puede provenir de otras fuentes, como por ejemplo: llamadas al backend. El requisito de nuestra aplicación es poder tener en tiempo real los últimos precios ofertados por nuestro producto. Para esto, vamos a usar eventos asincrónicos que propaguen información a nuestra interfaz de manera inmediata. Esto también conocido como Reactive Programming.
ObservableObject
Cuando creamos un ObservableObject estamos haciendo que una clase publique cambios de los datos que estamos manejando. Vamos a crear nuestro objeto Prices el cual va a mantener un listado de precios obtenidos desde una llamada al servidor.
class Prices: ObservableObject { @Published var prices: [Int] // ... obtain prices via API }
https://gist.github.com/Thonyvb/06dc3da1699bc8850ec11d8434e5c2f4
@Published
Uno de los property wrappers más útiles en el desarrollo de SwiftUI, @Published nos permite crear publicadores de eventos.
Con este property wrapper podemos publicar los cambios del listado de precios una vez que obtengamos nuevos datos desde nuestro servidor. Es decir; cuando existan cambios en el listado de prices, todas las interfaces de usuarios suscritas van a tener la última versión de los precios.
Y para poder completar el flujo de datos, nuestras vistas y/o subvistas necesitan tener una comunicación con el objeto Prices para poder obtener actualizaciones del listado de precios. Para ello, Apple nos entrega varias maneras de establecer la comunicación entre los ObservableObjects y nuestras vistas.
@StateObject
Lo usamos cuando la vista es dueña del ObservableObject, es decir que nuestra vista crea una nueva instancia.
En nuestro caso de uso, la vista que inicia el flujo es quien crea el ObservableObject Prices para que mantenga el estado de los últimos precios de la subasta. El syntax para realizarlo es de la siguiente manera en una vista:
struct MainView: View {
@StateObject var prices = Prices()
// …
https://gist.github.com/Thonyvb/247a1b6c8421986763dc365bb405b163
@ObservedObject
Si nosotros recibimos el ObservableObject de una vista padre, la subvista puede declarar un @ObservedObject
Por ejemplo, la vista principal que mantiene el ObservableObject con los precios de la subasta puede compartir este ObservableObject con su subvista que maneja el carrito de compras.
struct CartView: View { @ObservedObject var prices: Prices
// …
https://gist.github.com/Thonyvb/dc539d304da4c022f8de017eb9bba851
@EnvironmentObject
Si necesitamos compartir nuestro ObservableObject de precio entre varias sub vistas, podemos evitar el uso repetitivo de @ObservedObject al compartirlo una sola vez desde nuestra vista principal usando @EnvironmentObject
Esto nos ayuda a mantener una sola fuente de verdad, ya que todas las vistas van a reaccionar a los cambios de un solo ObservableObject.
struct MainView: View { @StateObject var prices = Prices() var body: some View { VStack { PricesView()
}.environmentObject(prices) } }
struct PricesView: View { @EnvironmentObject var prices: Prices
// …
https://gist.github.com/Thonyvb/cfee01d49540f79451270a466dd42716
Siguientes Pasos
El uso de los property wrappers es uno de los temas más desafiantes en SwiftUI, en especial porque requiere un análisis previo del diseño de la solución para poder usarlos de manera correcta y eficiente. Un uso inadecuado puede provocar múltiples re-renders de la interfaz de usuario que pueden impactar en los recursos del dispositivo, anti-patrones que pueden hacer el código muy difícil de mantener, o la pérdida de la reactividad de la interfaz, que implica que no aprovechamos el potencial de este framework. Una vez dominado los conceptos básicos de los property wrappers, un gran siguiente paso es explorar el framework de programación reactiva llamado Combine.
*Illustraciones por Xavier Idrovo
Fuentes:
https://developer.apple.com/documentation/swiftui/state-and-data-flow
Aviso legal: Las declaraciones y opiniones expresadas en este artículo son las del autor/a o autores y no reflejan necesariamente las posiciones de Thoughtworks.