Adaptando sua solução comercial existente para o SwiftUI. Parte 3. Trabalhando com Arquitetura

Bom Dia a todos! Com você, eu, Anna Zharkova, desenvolvedora móvel líder da Usetech,

continuamos a desmontar os meandros do SwiftUI. As partes anteriores podem ser encontradas nos links:

parte 1
parte 2

Hoje falaremos sobre os recursos da arquitetura e como transferir e incorporar a lógica de negócios existente no aplicativo SwiftUI.

O fluxo de dados padrão no SwiftUI é baseado na interação do View e um determinado modelo que contém propriedades e variáveis ​​de estado, ou que é essa variável de estado. Portanto, é lógico que o MVVM é o parceiro de arquitetura recomendado para aplicativos SwiftUI. A Apple sugere usá-lo em conjunto com a estrutura Combine, que apresenta a Api SwiftUI declarativa para processar valores ao longo do tempo. O ViewModel implementa o protocolo ObservableObject e se conecta como um ObservedObject a uma View específica.



As propriedades do modelo modificável são declaradas como @Published.

class NewsItemModel: ObservableObject {
    @Published var title: String = ""
    @Published var description: String = ""
    @Published var image: String = ""
    @Published var dateFormatted: String = ""
}

Como no MVVM clássico, o ViewModel se comunica com o modelo de dados (ou seja, lógica de negócios) e transfere os dados de uma forma ou de outra.

struct NewsItemContentView: View {
    @ObservedObject var moder: NewsItemModel
    
    init(model: NewsItemModel) {
        self.model = model 
    }
    //... - 
}

O MVVM, como quase qualquer outro padrão, tem tendência a congestionamento e
redundância. O ViewModel sobrecarregado sempre depende de quão bem a lógica de negócios é destacada e abstraída. A carga da visualização é determinada pela complexidade da dependência dos elementos em variáveis ​​de estado e transições para outra visualização.

No SwiftUI, o que é adicionado é que o View é uma estrutura, não uma classe e, portanto , não suporta herança, forçando a duplicação de código.
Se em pequenas aplicações isso não for crítico, com o aumento da funcionalidade e da complexidade da lógica, a sobrecarga se tornará crítica e uma grande quantidade de copiar e colar será inibida.

Vamos tentar usar a abordagem de código limpo e arquitetura limpa neste caso. Não podemos abandonar completamente o MVVM, afinal, o DataFlow SwiftUI é construído sobre ele, mas é um pouco para reconstruir.

Atenção!

Se você é alérgico a artigos sobre arquitetura, e o Código Limpo se inverte de uma frase, role alguns parágrafos.
Este não é exatamente um código limpo do tio Bob!


Sim, não tomaremos o Código Limpo do Tio Bob em sua forma mais pura. Quanto a mim, há um excesso de engenharia. Vamos ter apenas uma ideia.

A idéia principal do código limpo é criar o código mais legível, que pode ser expandido e modificado sem problemas.

Existem alguns princípios de desenvolvimento de software que devem ser seguidos.



Muitas pessoas os conhecem, mas nem todos os amam e nem todos os usam. Este é um tópico separado para o holivar.

Para garantir a pureza do código, pelo menos é necessário dividir o código em camadas e módulos funcionais, usar a solução geral de problemas e implementar a abstração da interação entre componentes. E pelo menos você precisa separar o código da interface do usuário da chamada lógica de negócios.

Independentemente do padrão arquitetural selecionado, a lógica de trabalhar com o banco de dados e a rede, o processamento e o armazenamento de dados é separada da interface do usuário e dos módulos do próprio aplicativo. Ao mesmo tempo, os módulos trabalham com implementações de serviços ou armazenamentos, que por sua vez acessam o serviço geral de solicitações de rede ou o armazenamento geral de dados. A inicialização das variáveis ​​pelas quais você pode acessar um ou outro serviço é realizada em um determinado contêiner geral, ao qual o módulo de aplicativo (lógica de negócios do módulo) acessa finalmente.



Se tivermos selecionado e abstraído a lógica de negócios, poderemos organizar a interação entre os componentes dos módulos conforme desejar.

Em princípio, todos os padrões existentes de aplicativos iOS operam no mesmo princípio.



Sempre há lógica de negócios, há dados. Há também um gerenciador de chamadas, responsável pela apresentação e transformação dos dados para saída e onde os dados convertidos são gerados. A única diferença é como as funções são distribuídas entre os componentes.


Porque nos esforçamos para tornar o aplicativo legível, para simplificar as mudanças atuais e futuras, é lógico separar todas essas funções. Nossa lógica de negócios já foi destacada, os dados são sempre separados. Permanecer expedidor, apresentador e visualização. Como resultado, obtemos uma arquitetura que consiste em View-Interactor-Presenter, na qual o interator interage com os serviços de lógica de negócios, o apresentador converte os dados e os fornece como uma espécie de ViewModel para o nosso View. De uma maneira boa, a navegação e a configuração também são removidas do View em componentes separados.



Obtemos a arquitetura VIP + R com a divisão de papéis controversos em diferentes componentes.

Vamos tentar ver um exemplo. Temos um pequeno aplicativo agregador de notícias
escrito em SwiftUI e MVVM.



A aplicação possui 3 telas separadas com lógica própria, ou seja, 3 módulos:


  • módulo de lista de notícias;
  • módulo de tela de notícias;
  • módulo de pesquisa de notícias.

Cada um dos módulos consiste em um ViewModel, que interage com a lógica de negócios selecionada, e um View, que exibe o que o ViewModel transmite para ele.



Nós nos esforçamos para garantir que o ViewModel se preocupe apenas com o armazenamento de dados prontos para exibição. Agora ele está envolvido no acesso aos serviços e no processamento dos resultados.

Transferimos essas funções para o apresentador e o interator, que configuramos para cada
módulo.



O interator passa os dados recebidos do serviço para o apresentador, o que preenche o ViewModel existente vinculado ao View com dados preparados. Em princípio, no que diz respeito à separação da lógica de negócios do módulo, tudo é simples. 


Agora vá para Visualizar. Vamos tentar lidar com a duplicação forçada de código. Se estamos lidando com algum tipo de controle, talvez sejam seus estilos ou configurações. Se estamos falando sobre a exibição na tela, então isto:

  • Estilos de tela
  • elementos comuns da interface do usuário (LoadingView);
  • alertas de informação;
  • alguns métodos gerais.

Não podemos usar herança, mas podemos usar bastante composição . É por esse princípio que todas as vistas personalizadas no SwiftUI são criadas.

Assim, criamos um contêiner View, no qual transferimos toda a mesma lógica, e passamos nossa View na tela para o inicializador de contêineres e depois a usamos como uma View de conteúdo no corpo.

struct ContainerView<Content>: IContainer, View where Content: View {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
    }

    var body : some View {
        ZStack {
            content
            if (self.containerModel.isLoading) {
                LoaderView()
            }
        }.alert(isPresented: $containerModel.hasError){
            Alert(title: Text(""), message: Text(containerModel.errorText),
                 dismissButton: .default(Text("OK")){
                self.containerModel.errorShown()
                })
        }
    }

A exibição na tela é incorporada ao ZStack dentro do corpo ContainerView, que também contém código para exibir o LoadingView e código para exibir um alerta de informações.

Também precisamos que o ContainerView receba um sinal do ViewModel da View interna e atualize seu estado. Não podemos assinar via @Observed no mesmo modelo
da Visualização interna, porque arrastaremos seus sinais.



Portanto, estabelecemos comunicação com ele através do padrão delegado e, para o estado atual do contêiner, usamos seu próprio ContainerModel.

class ContainerModel:ObservableObject {
    @Published var hasError: Bool = false
    @Published var errorText: String = ""
    @Published var isLoading: Bool = false
    
    func setupError(error: String){
     //....
       }
    
    func errorShown() {
     //...
    }
    
    func showLoading() {
        self.isLoading = true
    }
    
    func hideLoading() {
        self.isLoading = false
    }
}

O ContainerView implementa o protocolo IContainer; uma referência de instância é atribuída ao modelo de visualização incorporado.

protocol  IContainer {
    func showError(error: String)
    
    func showLoading()
    
    func hideLoading()
}

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
        self.content.viewModel?.listener = self
    }
    //- 
}

O View implementa o protocolo IModelView para encapsular o acesso ao modelo e unificar alguma lógica. Modelos para a mesma finalidade implementam o protocolo IModel:

protocol IModelView {
    var viewModel: IModel? {get}
}

protocol  IModel:class {
   //....
    var listener:IContainer? {get set}
}

Então, já neste modelo, se necessário, o método delegado é chamado, por exemplo, para exibir um alerta com um erro no qual a variável de estado do modelo de contêiner é alterada.

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    //- 
    func showError(error: String) {
        self.containerModel.setupError(error: error)
    }
    
    func showLoading() {
        self.containerModel.showLoading()
    }
    
    func hideLoading() {
        self.containerModel.hideLoading()
    }
}

Agora, podemos unificar o trabalho da View, alternando para trabalhar através do ContainerView.
Isso facilitará muito nossas vidas ao trabalhar com a configuração dos seguintes módulos e navegação.
Como configurar a navegação no SwiftUI e fazer uma configuração limpa, falaremos na próxima parte .

Você pode encontrar o código fonte do exemplo aqui .

All Articles