web-dev-qa-db-fra.com

SwiftUI - Comment passer EnvironmentObject dans View Model?

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 session d'application, par ex. 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 pour 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

15
Michael

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 séparés de la vue.

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

  1. iOS n'a pas de mécanisme de "liaison" dans le sens traditionnel Java sense.

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

    résout automatiquement tout; certains introduiraient 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. Tu

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

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 en Java façon SwiftUI. Et la façon 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.

1
Jim lai

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 ne passe ici et là que 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")
    }
}
1
Asperi

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

J'ai configuré mon projet avec un RootView et quelques vues enfant. J'ai configuré mon RootView avec un objet App 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 restaient petites et spécifiques. En guise de 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 MockApp qui 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
        }
    }

}
1
Michael Ozeryansky