SwiftUI - ¿Cómo pasar EnvironmentObject en View Model?

16

Estoy buscando crear un objeto de entorno al que pueda acceder el modelo de vista (no solo la vista).

El objeto Environment rastrea los datos de la sesión de la aplicación, por ejemplo, loginIn, token de acceso, etc., estos datos se pasarán a los modelos de vista (o clases de servicio cuando sea necesario) para permitir que una API pase datos de estos ObjectObjects.

He intentado pasar el objeto de sesión al iniciador de la clase de modelo de vista desde la vista, pero aparece un error.

¿Cómo puedo acceder / pasar el EnvironmentObject al modelo de vista usando SwiftUI?

Ver enlace al proyecto de prueba: https://gofile.io/?c=vgHLVx

Miguel
fuente
¿Por qué no pasar viewmodel como EO?
E.Coms
Parece exagerado, habrá muchos modelos de vista, la carga que he vinculado es solo un ejemplo simplificado
Michael
2
No estoy seguro de por qué esta pregunta fue rechazada, me pregunto lo mismo. Contestaré con lo que he hecho, espero que alguien más pueda encontrar algo mejor.
Michael Ozeryansky
2
@ E.Coms Esperaba que EnvironmentObject fuera generalmente un objeto. Conozco varios trabajos, parece un olor a código para hacerlos globalmente accesibles así.
Michael Ozeryansky
@Michael ¿Encontraste una solución para esto?
Brett

Respuestas:

3

Elijo no tener un ViewModel. (¿Quizás es hora de un nuevo patrón?)

He configurado mi proyecto con ay RootViewalgunas vistas secundarias. Configuré mi RootViewcon un Appobjeto como EnvironmentObject. En lugar de que ViewModel acceda a Modelos, todas mis vistas acceden a clases en la aplicación. En lugar de que ViewModel determine el diseño, la jerarquía de vistas determina el diseño. Al hacer esto en la práctica para algunas aplicaciones, he descubierto que mis puntos de vista siguen siendo pequeños y específicos. Como una simplificación excesiva:

class App {
   @Published var user = User()

   let networkManager: NetworkManagerProtocol
   lazy var userService = UserService(networkManager: networkManager)

   init(networkManager: NetworkManagerProtocol) {
      self.networkManager = networkManager
   }

   convenience init() {
      self.init(networkManager: NetworkManager())
   }
}
struct RootView {
    @EnvironmentObject var app: App

    var body: some View {
        if !app.user.isLoggedIn {
            LoginView()
        } else {
            HomeView()
        }
    }
}
struct HomeView: View {
    @EnvironmentObject var app: App

    var body: some View {
       VStack {
          Text("User name: \(app.user.name)")
          Button(action: { app.userService.logout() }) {
             Text("Logout")
          }
       }
    }
}

En mis avances, inicializo un MockAppque es una subclase de App. MockApp inicializa los inicializadores designados con el objeto Mocked. Aquí el UserService no necesita ser burlado, pero la fuente de datos (es decir, NetworkManagerProtocol) sí.

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            HomeView()
                .environmentObject(MockApp() as App) // <- This is needed for EnvironmentObject to treat the MockApp as an App Type
        }
    }

}
Michael Ozeryansky
fuente
Solo una nota: creo que es mejor evitar encadenar como app.userService.logout(). userServicedebe ser privado y acceder solo desde dentro de la clase de aplicación. El código anterior debería verse así: Button(action: { app.logout() })y la función de cierre de sesión llamará directamente userService.logout().
pawello2222 hace
@ pawello2222 No es mejor, es solo el patrón de la fachada sin ningún beneficio, pero puede hacer lo que desee.
Michael Ozeryansky
3

No deberías Es un error común pensar que SwiftUI funciona mejor con MVVM.

MVVM no tiene lugar en SwfitUI. Estás preguntando si puedes empujar un rectángulo para

encajar en forma de triángulo. No encajaría.

Comencemos con algunos hechos y trabajemos paso a paso:

  1. ViewModel es un modelo en MVVM.

  2. MVVM no tiene en cuenta el tipo de valor (p. Ej., No existe tal cosa en Java).

  3. Un modelo de tipo de valor (modelo sin estado) se considera más seguro que la referencia

    modelo de tipo (modelo con estado) en el sentido de inmutabilidad.

Ahora, MVVM requiere que configure un modelo de tal manera que siempre que cambie,

Actualiza la vista de alguna manera predeterminada. Esto se conoce como vinculante.

Sin vinculación, no tendrá una buena separación de preocupaciones, por ejemplo; refactorizando

modelo y estados asociados y mantenerlos separados de la vista.

Estas son las dos cosas que la mayoría de los desarrolladores MVVM de iOS fallan:

  1. iOS no tiene un mecanismo "vinculante" en el sentido tradicional de Java.

    Algunos simplemente ignorarían el enlace y pensarían en llamar a un objeto ViewModel

    resuelve todo de forma automática; algunos introducirían Rx basado en KVO, y

    complica todo cuando se supone que MVVM simplifica las cosas.

  2. modelo con estado es demasiado peligroso

    porque MVVM puso demasiado énfasis en ViewModel, muy poco en la gestión del estado

    y disciplinas generales en la gestión del control; la mayoría de los desarrolladores terminan

    pensar que un modelo con estado que se usa para actualizar la vista es reutilizable y

    comprobable .

    esta es la razón por la cual Swift introduce el tipo de valor en primer lugar; un modelo sin

    estado.

Ahora a su pregunta: ¿pregunta si su ViewModel puede tener acceso a EnvironmentObject (EO)?

No deberías Porque en SwiftUI un modelo que se ajusta a Ver automáticamente tiene

referencia a EO. P.ej;

struct Model: View {
    @EnvironmentObject state: State
    // automatic binding in body
    var body: some View {...}
}

Espero que la gente pueda apreciar cómo está diseñado el SDK compacto.

En SwiftUI, MVVM es automático . No hay necesidad de un objeto ViewModel separado

que se une manualmente a la vista que requiere que se le pase una referencia EO.

El código anterior es MVVM. P.ej; Un modelo con unión para ver.

Pero debido a que el modelo es un tipo de valor, en lugar de refactorizar el modelo y el estado como

Ver modelo, refactoriza el control (en la extensión de protocolo, por ejemplo).

Este es el SDK oficial que adapta el patrón de diseño a la función de idioma, en lugar de solo

haciendo cumplir Sustancia sobre la forma.

Mira tu solución, tienes que usar singleton que es básicamente global. usted

debe saber lo peligroso que es acceder a cualquier lugar global sin protección de

inmutabilidad, que no tiene porque tiene que usar el modelo de tipo de referencia.

TL; DR

No haces MVVM de manera Java en SwiftUI. Y la forma rápida de hacerlo no es necesario

para hacerlo, ya está incorporado.

Espero que más desarrolladores vean esto, ya que esto parecía una pregunta popular.

Jim lai
fuente
1

A continuación se proporciona un enfoque que funciona para mí. Probado con muchas soluciones comenzadas con Xcode 11.1.

El problema se originó por la forma en que EnvironmentObject se inyecta a la vista, esquema general

SomeView().environmentObject(SomeEO())

es decir, en la primera vista creada, en el segundo objeto del entorno creado, en el tercer objeto del entorno inyectado en la vista

Por lo tanto, si necesito crear / configurar el modelo de vista en el constructor de vistas, el objeto de entorno todavía no está presente.

Solución: separe todo y use inyección de dependencia explícita

Así es como se ve en el código (esquema genérico)

// somewhere, say, in SceneDelegate

let someEO = SomeEO()                            // create environment object
let someVM = SomeVM(eo: someEO)                  // create view model
let someView = SomeView(vm: someVM)              // create view 
                   .environmentObject(someEO)

No hay ninguna compensación aquí, porque ViewModel y EnvironmentObject son, por diseño, tipos de referencia (en realidad ObservableObject), por lo que paso aquí y allá solo referencias (también conocidos como punteros).

class SomeEO: ObservableObject {
}

class BaseVM: ObservableObject {
    let eo: SomeEO
    init(eo: SomeEO) {
       self.eo = eo
    }
}

class SomeVM: BaseVM {
}

class ChildVM: BaseVM {
}

struct SomeView: View {
    @EnvironmentObject var eo: SomeEO
    @ObservedObject var vm: SomeVM

    init(vm: SomeVM) {
       self.vm = vm
    }

    var body: some View {
        // environment object will be injected automatically if declared inside ChildView
        ChildView(vm: ChildVM(eo: self.eo)) 
    }
}

struct ChildView: View {
    @EnvironmentObject var eo: SomeEO
    @ObservedObject var vm: ChildVM

    init(vm: ChildVM) {
       self.vm = vm
    }

    var body: some View {
        Text("Just demo stub")
    }
}
Asperi
fuente