Property Wrapper ObservedObject y StateObject en SwiftUI
Property Wrapper ObservedObject y StateObject en SwiftUI

@ObservedObject y @StateObject en SWIFTUI en Español

ObservedObject y StateObject son dos property wrappers que usamos igual que State y Binding. Pero en lugar de estar dentro de las vistas, creamos una clase aparte. Esta clase conforma el protocolo ObservableObject y usa @Published en propiedades donde queremos escuchar cambios y así redibujar vistas

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
Aprende SwiftUI desde cero, Property Wrapper ObservedObject y StateObject
Aprende SwiftUI desde cero, Property Wrapper ObservedObject y StateObject

Hoy vamos a ver dos property wrappers: ObservedObject y StateObject (son parecidos a @State y @Binding que ya hemos visto) pero se utilizan de forma diferente.

En ObservedObject y StateObject, se crea una clase aparte con diferentes propiedades y métodos, estas propiedades se pueden usar en nuestras vistas y cuando cambian, la vista se redibuja. De esta manera podemos reutilizar esta clase entre diferentes vistas y todas pueden escuchar cambios en estas propiedades y así, como hemos dicho, redibujar sus vistas y estar totalmente sincronizadas.

Con @State y @Binding (este tipo de property wrappers) permanecian dentro de la vista, pero con @ObservedObject y @StateObject creamos un tipo que será una clase externa y por lo tanto tendremos que gestionarla nosotros, es decir: encargarnos de inicializarla, dando una valor inicial a sus propiedades y también podremos incluir métodos. En muchas ocasiones es mejor usar estas clases para mover todo el código relacionado y así no tener esta responsabilidad dentro de las vistas, esto lo veremos muy claro en unos minutos.

Para entender mejor cómo funcionan estos property wrappers vamos a mostrar en una misma vista:

  • Un contador
  • Un listado

El contador se incrementará cuando pulsemos en un Button, y el listado mostrará la temática de los videos de SwiftBeta en Youtube, hasta aquí todo muy sencillo.

Lo primero de todo que vamos a crear dentro de ContentView es, el contador:

struct ContentView: View {
    
    @State private var counter: Int = 0
    
    var body: some View {
        VStack {
            Text("Contador: \(counter)")
                .bold()
                .font(.largeTitle)
                .padding(.top, 12)
            Button("Incrementar Contador") {
                counter += 1
            }
            Spacer()
        }
    }
}
Código para crear un contador en SwiftUI

Como hemos dicho, un simple contador que cada vez que se pulse el botón se incrementará el valor y se mostrará el valor dentro del Text.

Ahora crearemos un listado mostrando la temática de los videos de SwiftBeta en Youtube, vamos a crear una vista llamada ListVideos:

struct ListVideos: View {
    
    @State private var videosModel: [String] = []
    
    var body: some View {
        NavigationView {
            List(videosModel, id: \.self) { video in
                Text(video)
            }
            .navigationTitle("SwiftBeta Videos")
            .navigationBarItems(leading:
                Button("Añadir", action: addMoreTopics)
            )
        }
        .onAppear {
            videosModel = ["Aprende SwiftUI",
                           "Aprende Xcode",
                           "Aprende Swift"
            ]
        }
    }
    
    func addMoreTopics() {
        videosModel.append("Aprende CI/CD")
        videosModel.append("Aprende Git"))
        videosModel.append("Aprende sobre Testing")
        videosModel.append("Aprende APIs de Apple")
    }
}
Código para crear un listado en SwiftUI

Lo que hemos hecho hasta ahora, es que cuando la vista ListVideos aparece, rellenamos datos en nuestra propiedad @State de tipo array de VideoModel. Por lo tanto, se muestra en la lista Aprende SwiftUI, Aprende Swift y Aprende Xcode. También, hemos creado un método addMoreTopics() que se ejecuta cuando se pulsa el botón izquierdo de la NavigationView llamado "Añadir".

Ahora, dentro de ContentView añadimos ListVideos debajo de nuestro Button para que se muestre nuestro Listado:

struct ContentView: View {
    
    @State private var counter: Int = 0
    
    var body: some View {
        VStack {
            Text("Contador: \(counter)")
                .bold()
                .font(.largeTitle)
                .padding(.top, 12)
            Button("Incrementar Contador") {
                counter += 1
            }
            ListVideos()
            Spacer()
        }
    }
}
Componemos la vista contador y el listado de videos

Perfecto, ya tenemos todas las piezas. Al compilar vemos el contador y justo debajo el listado. Pero fíjate que en ListVideos tenemos código que se podría mover a una clase, como por ejemplo el método addModeTopics().
Normalmente está información viene de alguna llamada a nuestro servidor, base de datos, etc. La vista no debería ser la responsable de esto, por eso vamos a crear una clase que se encargue de obtener y manipular esta información, y cuando esta información cambie, automáticamente la vista será actualizada.

ObservedObject

Y es aquí donde entra en juego ObservedObject. Vamos a crear una clase llamada VideoViewModel, con una propiedad videosModel, y el método addMoreTopics() que hemos visto antes, vamos a mover código a esta clase.

final class VideoViewModel {
    var videosModel: [String] = []
    
    init() {
        videosModel = ["Aprende SwiftUI",
                       "Aprende Xcode",
                       "Aprende Swift"]
    }
    
    func addMoreTopics() {
        videosModel.append("Aprende CI/CD")
        videosModel.append("Aprende Git")
        videosModel.append("Aprende sobre Testing")
        videosModel.append("Aprende APIs de Apple")
    }
}
Creamos nuestro ViewModel

Cuando inicializamos la clase, automáticamente le asignamos 3 valores al array. Así cuando la vista se cargue aparecerá "Aprende SwiftUI", "Aprende Xcode" y "Aprende Swift" por defecto en la List (y el código que teníamos en el onAppear lo podremos borrar).

Ahora vamos a ir a ListView y vamos a modificar la propiedad, vamos a eliminar @State y vamos a crear una instancia de nuestra nueva clase. Tendremos varios fallos, ya que tenemos que asignar la propiedad y método correcto:

struct ListVideos: View {
    
    private var videoViewModel = VideoViewModel()
    
    var body: some View {
        NavigationView {
            List(videoViewModel.videosModel, id: \.self) { video in
                Text(video)
            }
            .navigationTitle("SwiftBeta Videos")
            .navigationBarItems(leading:
                                    Button("Añadir", action: videoViewModel.addMoreTopics)
            )
        }
    }
}
Usamos nuestro ViewModel en ListVideos
Añadir los elementos a la propiedad en el .onAppear ya no es necesario, ya que los hemos añadido al instanciar la clase VideoViewModel.

Si compilamos la aplicación y pulsamos el botón Añadir del NavigationView qué pasa? No pasa nada. ¿Por qué? tenemos que hacer unos pequeños cambios.

Primero de todo debemos ir a nuestro VideoViewModel y conformar el protocolo ObservableObject. Justo después, nos vamos a la propiedad videosModel y debemos usar en este caso el property wrapper @Published.

@Published es muy parecido al @State, excepto que en lugar de usarse en una struct se usa en una class. Y cualquier cambio que se realice en esta propiedad se podrá controlar en las vistas donde tengamos una instancia de VideoViewModel.
Es decir, cuando llamemos a addMoreTopics() la variable videosModel será modificada y por lo tanto todas las vistas donde se use se refrescaran con la nueva información.

El código de VideoViewModel quedaría de la siguiente manera:

final class VideoViewModel: ObservableObject {
    @Published var videosModel: [String] = []
    
    init() {
        videosModel = ["Aprende SwiftUI",
                       "Aprende Xcode",
                       "Aprende Swift"]
    }
    
    func addMoreTopics() {
        videosModel.append("Aprende CI/CD")
        videosModel.append("Aprende Git")
        videosModel.append("Aprende sobre Testing")
        videosModel.append("Aprende APIs de Apple")
    }
}
Conformamos ObservableObject y usamos @Published

Nos falta un último paso, ahora debemos ir a ListVideos y en la propiedad videoViewModel debemos añadir @ObservedObject:

struct ListVideos: View {
    
    @ObservedObject private var videoViewModel = VideoViewModel()
    
    var body: some View {
        NavigationView {
            List(videoViewModel.videosModel, id: \.self) { video in
                Text(video)
            }
            .navigationTitle("SwiftBeta Videos")
            .navigationBarItems(leading:
                                    Button("Añadir", action: videoViewModel.addMoreTopics)
            )
        }
    }
}
Añadimos @ObservedObject a nuestra propiedad

Si compilamos, y probamos nuestro código parece que funciona.

1. Si pulsamos en el Button para incrementar el contador funciona ✅
2. Si pulsamos en el Button añadir, se añaden al listado 3 opciones más ✅
3. Si realizamos los pasos 1 y 2 y luego volvemos hacer el 1, en el listado se eliminan items y vuelven aparecer los 3 iniciales 🛑

¿Esto a qué es debido? Cada vez que pulsamos en el Button incrementar, la vista ContentView se redibuja, por lo tanto se crea una nueva instancia de ListVideos() y asigna a la propiedad @ObservedObject videoViewModel una nueva instancia de VideoViewModel, que recuerda, solo contiene 3 valores ("Aprende SwiftUI", "Aprende Xcode" y "Aprende Swift"). Como ves, esto es un problema ya que la información que habíamos introducido a desaparecido, PERO para arreglar esto, solo debemos hacer un pequeño cambio, y es cambiar el property wrapper de esta variable para que en lugar de ser @ObservedObject sea @StateObject

StateObject

Aquí modificamos el código y usamos el property wrapper @StateObject

struct ListVideos: View {
    
    @StateObject private var videoViewModel = VideoViewModel()
    
    var body: some View {
        NavigationView {
            List(videoViewModel.videosModel, id: \.self) { video in
                Text(video)
            }
            .navigationTitle("SwiftBeta Videos")
            .navigationBarItems(leading:
                                    Button("Añadir", action: videoViewModel.addMoreTopics)
            )
        }
    }
}
Usamos StateObject en SwiftUI

StateObject es lo mismo que un ObservedObject excepto que si la vista se renderiza de nuevo el estado de StateObject no se pierde.

Una regla que podemos utilizar es usar StateObject cuando es la primera vez que instanciamos esa clase y si lo pasamos a otra vista o subvista de nuestra app, ahí podemos usar ObservedObject.