SwiftUI - Wie übergebe ich EnvironmentObject an View Model?

16

Ich möchte ein EnvironmentObject erstellen, auf das das Ansichtsmodell zugreifen kann (nicht nur die Ansicht).

Das Umgebungsobjekt verfolgt die Anwendungssitzungsdaten, z. B. angemeldet, Zugriffstoken usw. Diese Daten werden an die Ansichtsmodelle (oder bei Bedarf an Serviceklassen) übergeben, damit eine API aufgerufen werden kann, um Daten von diesen EnvironmentObjects zu übergeben.

Ich habe versucht, das Sitzungsobjekt aus der Ansicht an den Initialisierer der Ansichtsmodellklasse zu übergeben, erhalte jedoch eine Fehlermeldung.

Wie kann ich mit SwiftUI auf das EnvironmentObject zugreifen / es an das Ansichtsmodell übergeben?

Siehe Link zum Testprojekt: https://gofile.io/?c=vgHLVx

Michael
quelle
Warum nicht viewmodel als EO übergeben?
E.Coms
Scheint übertrieben, es wird viele Ansichtsmodelle geben, der Upload, den ich verlinkt habe, ist nur ein vereinfachtes Beispiel
Michael
2
Ich bin mir nicht sicher, warum diese Frage abgelehnt wurde. Ich frage mich das auch. Ich werde mit dem antworten, was ich getan habe, hoffentlich kann sich jemand anderes etwas Besseres einfallen lassen.
Michael Ozeryansky
2
@ E.Coms Ich habe erwartet, dass EnvironmentObject im Allgemeinen ein Objekt ist. Ich kenne mehrere Arbeiten, es scheint ein Code-Geruch zu sein, um sie so global zugänglich zu machen.
Michael Ozeryansky
@Michael Hast du überhaupt eine Lösung dafür gefunden?
Brett

Antworten:

3

Ich habe kein ViewModel. (Vielleicht Zeit für ein neues Muster?)

Ich habe mein Projekt mit einer RootViewund einigen untergeordneten Ansichten eingerichtet. Ich richte mein Objekt RootViewmit einem AppObjekt als EnvironmentObject ein. Anstelle des ViewModel, das auf Modelle zugreift, greifen alle meine Ansichten auf Klassen in der App zu. Anstelle des ViewModel, das das Layout bestimmt, bestimmt die Ansichtshierarchie das Layout. Nachdem ich dies in der Praxis für einige Apps getan habe, habe ich festgestellt, dass meine Ansichten klein und spezifisch bleiben. Zur Vereinfachung:

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

In meinen Voransichten initialisiere ich MockAppeine Unterklasse von App. Die MockApp initialisiert die festgelegten Initialisierer mit dem verspotteten Objekt. Hier muss der UserService nicht verspottet werden, die Datenquelle (dh NetworkManagerProtocol) jedoch.

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
quelle
Nur eine Anmerkung: Ich denke, es ist besser, Verkettungen wie zu vermeiden app.userService.logout(). userServicesollte privat sein und nur innerhalb der App-Klasse aufgerufen werden. Der obige Code sollte folgendermaßen aussehen: Button(action: { app.logout() })Die Abmeldefunktion wird dann direkt aufgerufen userService.logout().
pawello2222 vor
@ pawello2222 Es ist nicht besser, es ist nur das Fassadenmuster ohne Nutzen, aber Sie können tun, was Sie wollen.
Michael Ozeryansky vor
3

Das solltest du nicht. Es ist ein weit verbreitetes Missverständnis, dass SwiftUI am besten mit MVVM funktioniert.

MVVM hat keinen Platz in SwfitUI. Sie fragen, ob Sie ein Rechteck auf schieben können

eine Dreiecksform anpassen. Es würde nicht passen.

Beginnen wir mit einigen Fakten und arbeiten Schritt für Schritt:

  1. ViewModel ist ein Modell in MVVM.

  2. MVVM berücksichtigt keinen Werttyp (z. B. in Java nicht).

  3. Ein Werttypmodell (Modell ohne Status) gilt als sicherer als Referenz

    Typmodell (Modell mit Zustand) im Sinne der Unveränderlichkeit.

In MVVM müssen Sie jetzt ein Modell so einrichten, dass es bei jeder Änderung geändert wird

aktualisiert die Ansicht auf eine vorher festgelegte Weise. Dies wird als Bindung bezeichnet.

Ohne Bindung haben Sie keine schöne Trennung von Bedenken, z. Refactoring aus

Modell und zugehörige Zustände und deren Trennung von der Ansicht.

Dies sind die beiden Dinge, die die meisten iOS MVVM-Entwickler versagen:

  1. iOS hat keinen "Bindungs" -Mechanismus im traditionellen Java-Sinne.

    Einige würden die Bindung einfach ignorieren und denken, ein Objekt ViewModel aufzurufen

    löst automatisch alles; Einige würden KVO-basierten Rx einführen, und

    komplizieren Sie alles, wenn MVVM die Dinge einfacher machen soll.

  2. Modell mit Staat ist einfach zu gefährlich

    weil MVVM zu viel Wert auf ViewModel legt, zu wenig auf die Statusverwaltung

    und allgemeine Disziplinen bei der Verwaltung der Kontrolle; Die meisten Entwickler landen am Ende

    Denken, ein Modell mit Status, der zum Aktualisieren der Ansicht verwendet wird, ist wiederverwendbar und

    testbar .

    Aus diesem Grund führt Swift in erster Linie den Werttyp ein. ein Modell ohne

    Zustand.

Nun zu Ihrer Frage: Sie fragen, ob Ihr ViewModel Zugriff auf EnvironmentObject (EO) haben kann?

Das solltest du nicht. Denn in SwiftUI hat ein Modell, das der Ansicht entspricht, automatisch

Verweis auf EO. Z.B;

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

Ich hoffe, die Leute können verstehen, wie kompakt das SDK ist.

In SwiftUI erfolgt MVVM automatisch . Es ist kein separates ViewModel-Objekt erforderlich

Das wird manuell an die Ansicht gebunden, für die eine EO-Referenz erforderlich ist.

Der obige Code ist MVVM. Z.B; ein Modell mit Bindung zur Ansicht.

Aber weil Modell ein Werttyp ist, anstatt Modell und Status als umzugestalten

Wenn Sie das Modell anzeigen, überarbeiten Sie die Kontrolle (z. B. in der Protokollerweiterung).

Dies ist das offizielle SDK, das das Designmuster an die Sprachfunktion anpasst und nicht nur

Durchsetzung. Substanz über Form.

Schauen Sie sich Ihre Lösung an, Sie müssen Singleton verwenden, das im Grunde global ist. Du

sollte wissen, wie gefährlich es ist, global überall ohne Schutz von zuzugreifen

Unveränderlichkeit, die Sie nicht haben, weil Sie Referenztyp-Modell verwenden müssen!

TL; DR

In SwiftUI wird MVVM nicht auf Java-Weise ausgeführt. Und der schnelle Weg, dies zu tun, ist nicht nötig

dafür ist es bereits eingebaut.

Hoffe, dass mehr Entwickler dies sehen, da dies eine beliebte Frage zu sein schien.

Jim lai
quelle
1

Unten finden Sie einen Ansatz, der für mich funktioniert. Getestet mit vielen Lösungen, die mit Xcode 11.1 gestartet wurden.

Das Problem ist auf die Art und Weise zurückzuführen, wie EnvironmentObject im allgemeinen Schema der Ansicht injiziert wird

SomeView().environmentObject(SomeEO())

dh in der ersten erstellten Ansicht, im zweiten erstellten Umgebungsobjekt, im dritten in die Ansicht eingefügten Umgebungsobjekt

Wenn ich also ein Ansichtsmodell im Ansichtskonstruktor erstellen / einrichten muss, ist das Umgebungsobjekt dort noch nicht vorhanden.

Lösung: Brechen Sie alles auseinander und verwenden Sie die explizite Abhängigkeitsinjektion

So sieht es im Code aus (generisches Schema)

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

Hier gibt es keinen Kompromiss, da ViewModel und EnvironmentObject von Natur aus Referenztypen sind (eigentlich ObservableObject), daher übergebe ich hier und da nur Referenzen (auch Zeiger genannt).

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
quelle