SwiftUI - jak przekazać EnvironmentObject do View Model?

16

Chcę utworzyć obiekt środowiska, do którego można uzyskać dostęp za pomocą modelu widoku (nie tylko widoku).

Obiekt Environment śledzi dane sesji aplikacji, np. LoggedIn, token dostępu itp., Dane te zostaną przekazane do modeli widoku (lub klas usług, jeśli to konieczne), aby umożliwić wywołanie interfejsu API w celu przekazania danych z tego obiektu ObjectObjects.

Próbowałem przekazać obiekt sesji do inicjalizatora klasy modelu widoku z widoku, ale wystąpił błąd.

jak mogę uzyskać dostęp / przekazać obiekt EnvironmentObject do modelu widoku za pomocą SwiftUI?

Zobacz link do projektu testowego: https://gofile.io/?c=vgHLVx

Michał
źródło
Dlaczego nie przekazać viewmodel jako EO?
E.Coms,
Wydaje się, że będzie na górze, będzie wiele modeli wyświetlania, przesłany link, który podłączyłem, jest tylko uproszczonym przykładem
Michael
2
Nie jestem pewien, dlaczego to pytanie zostało odrzucone, zastanawiam się nad tym samym. Odpowiem na to, co zrobiłem, mam nadzieję, że ktoś inny wymyśli coś lepszego.
Michael Ozeryansky
2
@ E.Coms Oczekiwałem, że EnvironmentObject będzie ogólnie jednym obiektem. Znam wiele prac, wydaje się, że zapach kodu sprawia, że ​​są globalnie dostępne w ten sposób.
Michael Ozeryansky
@Michael Czy znalazłeś rozwiązanie tego problemu?
Brett

Odpowiedzi:

3

Nie chcę mieć ViewModel. (Może czas na nowy wzór?)

Skonfigurowałem swój projekt z RootViewkilkoma widokami potomnymi. Konfiguracji i moje RootViewz Appobiektu jako EnvironmentObject. Zamiast ViewModel uzyskującego dostęp do modeli, wszystkie moje widoki uzyskują dostęp do klas w aplikacji. Zamiast ViewModel określającego układ, hierarchia widoku określa układ. Po zrobieniu tego w praktyce dla kilku aplikacji, stwierdziłem, że moje poglądy są niewielkie i szczegółowe. Jako nadmierne uproszczenie:

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

W moich zapowiedziach inicjuję MockApppodklasę App. MockApp inicjuje wyznaczone inicjatory za pomocą obiektu Mocked. Tutaj UserService nie musi być wyśmiewany, ale źródło danych (tj. NetworkManagerProtocol) robi to.

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
źródło
Tylko uwaga: myślę, że lepiej unikać takich łańcuchów app.userService.logout(). userServicepowinny być prywatne i dostępne tylko z poziomu klasy aplikacji. Powyższy kod powinien wyglądać następująco: Button(action: { app.logout() })funkcja wylogowania zadzwoni bezpośrednio userService.logout().
pawello2222
@ pawello2222 Nie jest lepiej, to tylko wzór fasady bez żadnych korzyści, ale możesz robić, co chcesz.
Michael Ozeryansky
3

Nie powinieneś To powszechne błędne przekonanie, że SwiftUI działa najlepiej z MVVM.

MVVM nie ma miejsca w SwfitUI. Pytasz, czy możesz przesunąć prostokąt

pasują do kształtu trójkąta. Nie pasowałoby.

Zacznijmy od kilku faktów i pracuj krok po kroku:

  1. ViewModel to model w MVVM.

  2. MVVM nie bierze pod uwagę typu wartości (np. Nie ma czegoś takiego w java).

  3. Model typu wartości (model bez stanu) jest uważany za bezpieczniejszy niż referencyjny

    model typu (model ze stanem) w sensie niezmienności.

Teraz MVVM wymaga skonfigurowania modelu w taki sposób, aby za każdym razem, gdy się zmieniał

aktualizuje widok w określony sposób. Jest to znane jako wiązanie.

Bez wiązania nie będziesz miał przyjemnego rozdziału problemów, np. refaktoryzacja

model i skojarzone stany oraz oddzielając je od widoku.

Są to dwie rzeczy, których zawodzi większość programistów iOS MVVM:

  1. iOS nie ma mechanizmu „wiązania” w tradycyjnym sensie Java.

    Niektórzy po prostu zignorują powiązanie i pomyślą o wywołaniu obiektu ViewModel

    automagicznie rozwiązuje wszystko; niektórzy wprowadziliby Rx oparte na KVO i

    komplikuj wszystko, gdy MVVM ma uprościć sprawę.

  2. model ze stanem jest po prostu zbyt niebezpieczny

    ponieważ MVVM kładzie zbyt duży nacisk na ViewModel, zbyt mało na zarządzanie stanem

    oraz ogólne dyscypliny w zarządzaniu Kontrolą; większość programistów kończy

    myślenie, że model ze stanem używanym do aktualizacji widoku jest wielokrotnego użytku i

    testowalny .

    dlatego Swift przede wszystkim wprowadza rodzaj wartości; model bez

    stan.

Teraz pytanie: pytasz, czy Twój ViewModel może mieć dostęp do EnvironmentObject (EO)?

Nie powinieneś Ponieważ w SwiftUI automatycznie ma model zgodny z View

odniesienie do EO. Na przykład;

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

Mam nadzieję, że ludzie docenią projekt kompaktowego zestawu SDK.

W SwiftUI MVVM działa automatycznie . Nie ma potrzeby oddzielnego obiektu ViewModel

który ręcznie łączy się z widokiem, który wymaga przekazania do niego referencji EO.

Powyższy kod to MVVM. Na przykład; model z wiązaniem do wyświetlenia.

Ale ponieważ model jest typem wartości, więc zamiast refaktoryzować model i podać stan jako

zobacz model, refaktoryzujesz kontrolę (na przykład w rozszerzeniu protokołu).

Jest to oficjalny zestaw SDK, dostosowujący wzór projektowy do funkcji języka, a nie tylko

egzekwowanie tego. Substancja nad formą.

Spójrz na swoje rozwiązanie, musisz użyć singletona, który jest zasadniczo globalny. ty

powinien wiedzieć, jak niebezpieczny jest dostęp globalny w dowolnym miejscu bez ochrony

niezmienność, której nie masz, ponieważ musisz użyć modelu typu referencyjnego!

TL; DR

Nie wykonujesz MVVM w języku Java w SwiftUI. A szybki sposób na zrobienie tego nie jest potrzebny

aby to zrobić, jest już wbudowany.

Mam nadzieję, że więcej programistów to zobaczy, ponieważ wydawało się to popularnym pytaniem.

Jim Lai
źródło
1

Poniżej podano podejście, które działa dla mnie. Testowane z wieloma rozwiązaniami rozpoczętymi w Xcode 11.1.

Problem pochodzi ze sposobu, w jaki wtłaczany jest widok EnvironmentObject, ogólny schemat

SomeView().environmentObject(SomeEO())

tzn. w pierwszym utworzonym widoku, w drugim utworzonym obiekcie środowiska, w trzecim obiekcie środowiska wstrzykiwanym do widoku

Dlatego jeśli muszę utworzyć / skonfigurować model widoku w konstruktorze widoku, obiekt środowiska jeszcze tam nie ma.

Rozwiązanie: rozbij wszystko na części i użyj jawnego zastrzyku zależności

Oto jak to wygląda w kodzie (schemat ogólny)

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

Nie ma tu żadnego kompromisu, ponieważ ViewModel i EnvironmentObject są z założenia typami referencyjnymi (tak naprawdę ObservableObject), więc przekazuję tu i tam tylko referencje (inaczej wskaźniki).

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
źródło