SwiftUI - Como passar o EnvironmentObject no modelo de exibição?

16

Estou procurando criar um EnvironmentObject que possa ser acessado pelo modelo de exibição (não apenas pela exibição).

O objeto Environment controla os dados da sessão do aplicativo, por exemplo, logon, token de acesso etc., esses dados serão passados ​​para os modelos de exibição (ou classes de serviço, quando necessário) para permitir a chamada de uma API para transmitir dados desse EnvironmentObjects.

Eu tentei passar o objeto de sessão para o inicializador da classe de modelo de exibição da exibição, mas obtive um erro.

como posso acessar / transmitir o EnvironmentObject no modelo de exibição usando o SwiftUI?

Consulte o link para testar o projeto: https://gofile.io/?c=vgHLVx

Michael
fonte
Por que não passar o viewmodel como EO?
E.Coms
Parece por cima, haverá muitos modelos de visualização, o upload que eu vinculei é apenas um exemplo simplificado #
Michael
2
Não sei por que essa pergunta foi rejeitada, estou me perguntando o mesmo. Vou responder com o que fiz, espero que outra pessoa possa propor algo melhor.
Michael Ozeryansky
2
@ E.Coms Eu esperava que o EnvironmentObject geralmente fosse um objeto. Conheço vários trabalhos, parece um cheiro de código para torná-los acessíveis globalmente assim.
Michael Ozeryansky
@ Michael Você encontrou uma solução para isso?
Brett

Respostas:

3

Eu escolho não ter um ViewModel. (Talvez seja hora de um novo padrão?)

Eu configurei meu projeto com uma RootViewe algumas exibições filho. Eu configurei o meu RootViewcom um Appobjeto como o EnvironmentObject. Em vez de o ViewModel acessar modelos, todas as minhas visualizações acessam as classes no aplicativo. Em vez de o ViewModel determinar o layout, a hierarquia da visualização determina o layout. Ao fazer isso na prática para alguns aplicativos, descobri que meus pontos de vista são pequenos e específicos. Como uma simplificação excessiva:

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

Nas minhas visualizações, inicializo um MockAppque é uma subclasse de App. O MockApp inicializa os inicializadores designados com o objeto Mocked. Aqui, o UserService não precisa ser ridicularizado, mas a fonte de dados (por exemplo, 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
        }
    }

}
Michael Ozeryansky
fonte
Apenas uma observação: acho melhor evitar encadeamento app.userService.logout(). userServicedeve ser privado e acessado apenas de dentro da classe do aplicativo. O código acima deve ter a seguinte aparência: Button(action: { app.logout() })e a função de logoff será chamada diretamente userService.logout().
pawello2222
@ pawello2222 Não é melhor, é apenas o padrão da fachada sem nenhum benefício, mas você pode fazer o que quiser.
Michael Ozeryansky
3

Você não deveria. É um equívoco comum que o SwiftUI funcione melhor com o MVVM.

MVVM não tem lugar no SwfitUI. Você está perguntando se pode empurrar um retângulo para

ajuste uma forma de triângulo. Não caberia.

Vamos começar com alguns fatos e trabalhar passo a passo:

  1. ViewModel é um modelo no MVVM.

  2. O MVVM não leva em consideração o tipo de valor (por exemplo, não existe tal coisa em java).

  3. Um modelo de tipo de valor (modelo sem estado) é considerado mais seguro que a referência

    modelo de tipo (modelo com estado) no sentido de imutabilidade.

Agora, o MVVM exige que você configure um modelo de forma que, sempre que ele for alterado,

atualiza a visualização de alguma maneira predeterminada. Isso é conhecido como ligação.

Sem ligação, você não terá uma boa separação de preocupações, por exemplo; refatorando

modelo e estados associados e mantendo-os separados da vista.

Estas são as duas coisas que a maioria dos desenvolvedores de MVVM para iOS falha:

  1. O iOS não possui mecanismo de "ligação" no sentido tradicional de java.

    Alguns simplesmente ignoram a ligação e acham que chamar um objeto ViewModel

    resolve automagicamente tudo; alguns introduziriam o RX baseado em KVO e

    complicar tudo quando o MVVM deve simplificar as coisas.

  2. modelo com estado é muito perigoso

    porque o MVVM coloca muita ênfase no ViewModel, muito pouco no gerenciamento de estado

    e disciplinas gerais no gerenciamento do controle; a maioria dos desenvolvedores acaba

    pensar que um modelo com estado usado para atualizar a vista é reutilizável e

    testável .

    é por isso que o Swift introduz o tipo de valor em primeiro lugar; um modelo sem

    Estado.

Agora, sua pergunta: você pergunta se seu ViewModel pode ter acesso ao EnvironmentObject (EO)?

Você não deveria. Como no SwiftUI, um modelo em conformidade com o View automaticamente

referência ao OE. Por exemplo;

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

Espero que as pessoas possam apreciar como o SDK compacto é projetado.

No SwiftUI, o MVVM é automático . Não há necessidade de um objeto ViewModel separado

que se liga manualmente à exibição que requer uma referência de EO passada para ele.

O código acima é MVVM. Por exemplo; um modelo com ligação para visualizar.

Mas como modelo é do tipo valor, em vez de refatorar o modelo e o estado como

Para visualizar o modelo, você refatora o controle (na extensão do protocolo, por exemplo).

Este é o SDK oficial que adapta o padrão de design ao recurso de idioma, em vez de apenas

aplicá-lo. Substância sobre a forma.

Olhe para a sua solução, você precisa usar o singleton, que é basicamente global. Vocês

deve saber o quão perigoso é acessar global em qualquer lugar sem a proteção de

imutabilidade, que você não possui porque precisa usar o modelo de tipo de referência!

TL; DR

Você não faz MVVM em java no SwiftUI. E a maneira rápida de fazer isso não é necessária

para isso, já está embutido.

Espero que mais desenvolvedores vejam isso, pois essa parecia uma pergunta popular.

Jim lai
fonte
1

Abordagem fornecida abaixo que funciona para mim. Testado com muitas soluções iniciadas no Xcode 11.1.

O problema teve origem na maneira como o EnvironmentObject é injetado na vista, esquema geral

SomeView().environmentObject(SomeEO())

ou seja, na primeira visão criada, no segundo objeto de ambiente criado, no terceiro objeto de ambiente injetado na vista

Portanto, se eu precisar criar / configurar o modelo de exibição no construtor de exibição, o objeto de ambiente ainda não está presente.

Solução: divida tudo e use injeção de dependência explícita

Aqui está como ele aparece no código (esquema genérico)

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

Não há nenhuma troca aqui, porque ViewModel e EnvironmentObject são, por design, tipos de referência (na verdade ObservableObject), então eu passo aqui e ali apenas referências (também conhecidas como ponteiros).

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
fonte