SwiftUI - Come passare EnvironmentObject nel modello di visualizzazione?


16

Sto cercando di creare un EnvironmentObject a cui può accedere il View Model (non solo la view).

L'oggetto Environment tiene traccia dei dati della sessione dell'applicazione, ad es. Login, token di accesso, ecc., Questi dati verranno passati ai modelli di vista (o classi di servizio ove necessario) per consentire la chiamata di un'API per passare i dati da questo EnvironmentObjects.

Ho provato a passare l'oggetto di sessione all'inizializzatore della classe del modello di vista dalla vista ma ho riscontrato un errore.

come posso accedere / passare EnvironmentObject nel modello di visualizzazione usando SwiftUI?

Vedi link per testare il progetto: https://gofile.io/?c=vgHLVx


Perché non passare il modello di visualizzazione come EO?
E.Coms

Sembra sopra, ci saranno molti modelli di visualizzazione, il caricamento che ho collegato è solo un esempio semplificato
Michael,

2
Non sono sicuro del motivo per cui questa domanda è stata sottoposta a voto negativo, mi chiedo lo stesso. Risponderò con quello che ho fatto, speriamo che qualcun altro possa inventare qualcosa di meglio.
Michael Ozeryansky, il

2
@ E.Coms Mi aspettavo che EnvironmentObject fosse generalmente un oggetto. Conosco lavori multipli, sembra un odore di codice per renderli accessibili a livello globale in quel modo.
Michael Ozeryansky, il

@Michael Hai mai trovato una soluzione a questo?
Brett,

Risposte:


3

Ho scelto di non avere un ViewModel. (Forse è tempo per un nuovo modello?)

Ho impostato il mio progetto con una RootViewe alcune viste figlio. Ho impostato il mio RootViewcon un Appoggetto come EnvironmentObject. Invece del ViewModel che accede ai modelli, tutte le mie visualizzazioni accedono alle classi sull'app. Invece del ViewModel che determina il layout, la gerarchia della vista determina il layout. Facendo questo in pratica per alcune app, ho scoperto che i miei punti di vista sono piccoli e specifici. Come semplificazione eccessiva:

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

Nelle mie anteprime, inizializzo un MockAppche è una sottoclasse di App. MockApp inizializza gli inizializzatori designati con l'oggetto Mocked. Qui UserService non ha bisogno di essere deriso, ma lo fa l'origine dati (cioè NetworkManagerProtocol).

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

}

Solo una nota: penso che sia meglio evitare il concatenamento come app.userService.logout(). userServicedovrebbe essere privato e accessibile solo dall'interno della classe dell'app. Il codice sopra dovrebbe apparire così: Button(action: { app.logout() })e quindi la funzione di logout chiamerà direttamente userService.logout().
pawello2222

@ pawello2222 Non è meglio, è solo il modello di facciata senza alcun vantaggio, ma puoi fare come desideri.
Michael Ozeryansky

3

Non dovresti. È un'idea sbagliata comune che SwiftUI funzioni meglio con MVVM.

MVVM non ha posto in SwfitUI. Stai chiedendo se puoi spingere un rettangolo su

misura una forma triangolare. Non si adatterebbe.

Cominciamo con alcuni fatti e lavoriamo passo dopo passo:

  1. ViewModel è un modello in MVVM.

  2. MVVM non prende in considerazione il tipo di valore (ad esempio, nessuna cosa simile in Java).

  3. Un modello del tipo di valore (modello senza stato) è considerato più sicuro del riferimento

    tipo di modello (modello con stato) nel senso di immutabilità.

Ora, MVVM richiede di impostare un modello in modo tale che ogni volta che cambi, esso

gli aggiornamenti vengono visualizzati in un modo predeterminato. Questo è noto come vincolante.

Senza vincolo, non avrai una buona separazione delle preoccupazioni, ad es. refactoring fuori

modello e stati associati e mantenendoli separati dalla vista.

Queste sono le due cose che la maggior parte degli sviluppatori iOS MVVM non riesce:

  1. iOS non ha alcun meccanismo "vincolante" nel senso tradizionale di Java.

    Alcuni ignorerebbero semplicemente l'associazione e penserebbero di chiamare un oggetto ViewModel

    risolve automagicamente tutto; alcuni introdurrebbero Rx basato su KVO e

    complicare tutto quando MVVM dovrebbe semplificare le cose.

  2. il modello con stato è troppo pericoloso

    perché MVVM pone troppa enfasi su ViewModel, troppo poco sulla gestione dello stato

    e discipline generali nella gestione del controllo; la maggior parte degli sviluppatori finisce

    pensare che un modello con stato utilizzato per aggiornare la vista sia riutilizzabile e

    testabile .

    questo è il motivo per cui Swift introduce innanzitutto il tipo di valore; un modello senza

    stato.

Ora alla tua domanda: chiedi se ViewModel può avere accesso a EnvironmentObject (EO)?

Non dovresti. Perché in SwiftUI è automaticamente presente un modello conforme a Visualizza

riferimento a EO. Per esempio;

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

Spero che le persone possano apprezzare la progettazione dell'SDK compatto.

In SwiftUI, MVVM è automatico . Non è necessario un oggetto ViewModel separato

che si lega manualmente alla visualizzazione che richiede un riferimento EO passato ad esso.

Il codice sopra è MVVM. Per esempio; un modello con rilegatura da visualizzare.

Ma poiché il modello è un tipo di valore, quindi invece di eseguire il refactoring di modello e dichiarare come

guarda il modello, rifatti il ​​controllo (ad esempio nell'estensione del protocollo).

Questo è l'SDK ufficiale che adatta il modello di progettazione alle funzionalità del linguaggio, piuttosto che solo

applicandolo. Sostanza più che forma.

Guarda la tua soluzione, devi usare singleton che è sostanzialmente globale. voi

dovrebbe sapere quanto sia pericoloso accedere a livello globale ovunque senza protezione

immutabilità, che non hai perché devi usare un modello di riferimento!

TL; DR

Non fai MVVM in modo java in SwiftUI. E il modo Swift-y per farlo non è necessario

per farlo, è già integrato.

Spero che più sviluppatori vedano questo dato che questa sembra una domanda popolare.


1

Di seguito viene fornito un approccio che funziona per me. Testato con molte soluzioni iniziate con Xcode 11.1.

Il problema ha avuto origine dal modo in cui EnvironmentObject viene iniettato in vista, schema generale

SomeView().environmentObject(SomeEO())

cioè, nella prima vista creata, nel secondo oggetto ambiente creato, nel terzo oggetto ambiente iniettato nella vista

Pertanto, se devo creare / configurare il modello di vista nel costruttore vista, l'oggetto ambiente non è ancora presente lì.

Soluzione: separare tutto e utilizzare l'iniezione di dipendenza esplicita

Ecco come appare nel codice (schema generico)

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

Non c'è alcun compromesso qui, perché ViewModel e EnvironmentObject sono, in base alla progettazione, tipi di riferimento (in realtà, ObservableObject), quindi passo qua e là solo riferimenti (aka puntatori).

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")
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.