SwiftUI - Comment passer EnvironmentObject dans View Model?

16

Je cherche à créer un EnvironmentObject auquel le modèle de vue peut accéder (pas seulement la vue).

L'objet Environnement suit les données de la session d'application, par exemple, LogIn, jeton d'accès, etc., ces données seront transmises aux modèles de vue (ou aux classes de service si nécessaire) pour permettre à l'appel d'une API de transmettre les données de ces EnvironmentObjects.

J'ai essayé de passer l'objet de session à l'initialiseur de la classe de modèle de vue à partir de la vue, mais j'obtiens une erreur.

comment puis-je accéder à / passer l'EnvironnementObjet dans le modèle de vue à l'aide de SwiftUI?

Voir le lien pour tester le projet: https://gofile.io/?c=vgHLVx

Michael
la source
Pourquoi ne pas passer le viewmodel en tant qu'OE?
E.Coms
Semble au-dessus, il y aura de nombreux modèles de vue, le téléchargement que j'ai lié n'est qu'un exemple simplifié
Michael
2
Je ne sais pas pourquoi cette question a été rejetée, je me demande la même chose. Je répondrai avec ce que j'ai fait, j'espère que quelqu'un d'autre pourra trouver quelque chose de mieux.
Michael Ozeryansky
2
@ E.Coms Je m'attendais à ce que EnvironmentObject soit généralement un seul objet. Je connais plusieurs travaux, cela ressemble à une odeur de code pour les rendre globalement accessibles comme ça.
Michael Ozeryansky
@Michael Avez-vous même trouvé une solution à cela?
Brett

Réponses:

3

Je choisis de ne pas avoir de ViewModel. (Peut-être le temps d'un nouveau modèle?)

J'ai configuré mon projet avec une RootViewet quelques vues enfant. J'ai configuré mon RootViewavec un Appobjet comme EnvironmentObject. Au lieu que le ViewModel accède aux modèles, toutes mes vues accèdent aux classes sur l'application. Au lieu que le ViewModel détermine la disposition, la hiérarchie des vues détermine la disposition. Après avoir fait cela dans la pratique pour quelques applications, j'ai trouvé que mes vues restent petites et spécifiques. Comme simplification excessive:

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")
          }
       }
    }
}

Dans mes aperçus, j'initialise un MockAppqui est une sous-classe de App. Le MockApp initialise les initialiseurs désignés avec l'objet Mocked. Ici, le UserService n'a pas besoin d'être moqué, mais la source de données (c'est-à-dire NetworkManagerProtocol) le fait.

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
la source
Juste une note: je pense qu'il vaut mieux éviter de chaîner comme app.userService.logout(). userServicedoit être privé et accessible uniquement depuis l'intérieur de la classe d'application. Le code ci-dessus devrait ressembler à ceci:Button(action: { app.logout() }) et la fonction de déconnexion appellera alors directement userService.logout().
pawello2222
@ pawello2222 Ce n'est pas mieux, c'est juste le motif de la façade sans aucun avantage, mais vous pouvez faire ce que vous voulez.
Michael Ozeryansky Il y a
3

Tu ne devrais pas. C'est une idée fausse que SwiftUI fonctionne mieux avec MVVM.

MVVM n'a pas sa place dans SwfitUI. Vous demandez que si vous pouvez pousser un rectangle

s'adapter à une forme de triangle. Ça ne rentrerait pas.

Commençons par quelques faits et travaillons étape par étape:

  1. ViewModel est un modèle dans MVVM.

  2. MVVM ne prend pas en compte le type de valeur (par exemple, rien de tel en java).

  3. Un modèle de type valeur (modèle sans état) est considéré comme plus sûr que la référence

    modèle de type (modèle avec état) au sens d'immuabilité.

Maintenant, MVVM vous oblige à configurer un modèle de telle sorte que chaque fois qu'il change, il

met à jour la vue d'une manière prédéterminée. C'est ce qu'on appelle la liaison.

Sans engagement, vous n'aurez pas une bonne séparation des préoccupations, par exemple; refactoring out

modèle et les états associés et en les gardant à l'écart

Ce sont les deux choses que la plupart des développeurs MVVM iOS échouent:

  1. iOS n'a pas de mécanisme de «liaison» au sens traditionnel de Java.

    Certains ignoreraient simplement la liaison et penseraient à appeler un objet ViewModel

    résout automatiquement tout; certains introduiraient le Rx basé sur KVO, et

    complique tout lorsque MVVM est censé simplifier les choses.

  2. modèle avec état est tout simplement trop dangereux

    parce que MVVM met trop l'accent sur ViewModel, trop peu sur la gestion des états

    et disciplines générales dans la gestion du contrôle; la plupart des développeurs finissent par

    penser qu'un modèle avec un état utilisé pour mettre à jour la vue est réutilisable et

    testable .

    c'est pourquoi Swift introduit le type de valeur en premier lieu; un modèle sans

    Etat.

Maintenant à votre question: vous demandez si votre ViewModel peut avoir accès à EnvironmentObject (EO)?

Tu ne devrais pas. Parce que dans SwiftUI, un modèle conforme à View a automatiquement

référence à l'OE. Par exemple;

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

J'espère que les gens pourront apprécier la conception du SDK compact.

Dans SwiftUI, MVVM est automatique . Il n'y a pas besoin d'un objet ViewModel séparé

qui se lie manuellement à la vue qui nécessite une référence EO qui lui est transmise.

Le code ci-dessus est MVVM. Par exemple; un modèle avec reliure à voir.

Mais parce que le modèle est un type de valeur, au lieu de refactoriser le modèle et l'état comme

voir le modèle, vous refactorisez le contrôle (dans l'extension de protocole, par exemple).

Il s'agit du modèle de conception du SDK qui s'adapte aux fonctionnalités linguistiques, plutôt que simplement

le faire respecter. La substance plus que la forme.

Regardez votre solution, vous devez utiliser singleton qui est fondamentalement global. Vous

devrait savoir à quel point il est dangereux d'accéder au monde entier sans protection des

l'immuabilité, que vous n'avez pas car vous devez utiliser un modèle de type référence!

TL; DR

Vous ne faites pas MVVM de manière java dans SwiftUI. Et la manière Swift-y de le faire n'est pas nécessaire

pour le faire, il est déjà intégré.

J'espère que plus de développeurs verront cela car cela semblait être une question populaire.

Jim lai
la source
1

Ci-dessous une approche qui fonctionne pour moi. Testé avec de nombreuses solutions démarrées avec Xcode 11.1.

Le problème provient de la façon dont EnvironmentObject est injecté dans la vue, schéma général

SomeView().environmentObject(SomeEO())

c'est-à-dire, à la première vue créée, au deuxième objet environnement créé, au troisième objet environnement injecté dans la vue

Ainsi, si j'ai besoin de créer / configurer un modèle de vue dans le constructeur de vues, l'objet environnement n'y est pas encore présent.

Solution: séparez tout et utilisez l'injection de dépendance explicite

Voici à quoi cela ressemble dans le code (schéma générique)

// 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)

Il n'y a aucun compromis ici, car ViewModel et EnvironmentObject sont, par conception, des types de référence (en fait ObservableObject), donc je passe ici et là uniquement des références (aka pointeurs).

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
la source