SwiftUI e arquiteturas: VIPER e Clean Swift

Compartilhe

Compartilhar no facebook
Compartilhar no google
Compartilhar no twitter
Compartilhar no linkedin

Você provavelmente já sabe disso, mas esse artigo faz parte de uma série de artigos sobre como integrar SwiftUI em diferentes arquiteturas. Nos artigos anteriores, eu falei sobre MVC e MVP, e agora vou falar sobre Viper e Clean Swift.

Até o momento, eu estava cobrindo uma arquitetura por artigo, mas como essas duas arquiteturas possuem componentes bastante similares e as mudanças necessárias para integrar elas com SwiftUI são basicamente as mesmas, vou mostrar um exemplo detalhado de como fazer a integração com VIPER e, depois, explicar de forma simplificada sobre Clean Swift.

Eu vou explicar tudo que eu considerar necessário para que você entenda as arquiteturas, mas um pouco de conhecimento anterior sobre padrões de arquiteturas iOS e sobre o framework Combine podem ajudar.

Mas, começando pelo começo, como essas arquiteturas funcionam?

VIPER

No VIPER, nós temos 5 componentes diferentes com as seguintes responsabilidades:

  • View: Renderiza a tela e recebe ações do usuário. Quando está utilizando UIKit, o UIViewController faz parte da view.
  • Interactor: Responsável por regras de negócio. Ele decide como lidar com actions, acessa workers e data providers para obter os dados, chama gerenciadores de persistência para persistir informações, e informa o presenter sobre o que deve ser apresentado.
  • Presenter: Recebe ações da view e envia para o interactor lidar com elas. Depois, recebe modelos a partir do interactor e converte em outros modelos (ViewModels) que são enviados para a view renderizar as informações na tela. ViewModels devem possuir apenas informações que são necessárias para a view.
  • Entity: Objetos simples que representam dados e não realizam nenhuma lógica.
  • Router ou coordinator: controlam a navegação, instanciando a próxima tela e seus componentes.

Então, o fluxo de dados é basicamente esse:

image01

Podem existir algumas variações sobre qual componente possui a referência para o coordinator. Ela pode estar no ViewController, no interactor ou no presenter.

Clean Swift

image02

No clean swift os componentes são os mesmos, mas a forma como eles se comunicam muda. A view envia ações diretamente para o interactor, ao invés do presenter. 

Consequentemente, nós temos um fluxo unidirecional de dados entre a View, o Interactor e o Presenter: novamente, podemos ter variações sobre qual componente tem referência pro coordinator

Interface dos componentes

Nas duas arquiteturas a comunicação é realizada usando protocolos, tornando mais fácil testar cada componente e controlar os métodos que cada um expõe.

Eu gosto de nomear as interfaces de acordo com o padrão do exemplo abaixo, no qual nós temos uma tela de um carrinho de compras com:

  • Uma lista de itens
  • O resumo da venda
  • Um botão para finalizar a venda

Se o usuário está logado quando o botão é pressionado, a compra é finalizada, caso contrário a tela de login é apresentada.

image03

Algumas pessoas preferem nomear as interfaces de acordo com o padrão

ShoppingCartPresenterInput em vez de ShoppingCartPresenterProtocol e ShoppingCartPresenterOutput em vez de ShoppingCartPresenterDelegate. Outras preferem ShoppingCartViewToPresenterProtocol e ShoppingCartPresenterToViewProtocol respectivamente, indicando a direção da comunicação. Sinta-se livre para escolher.

O OrderWorker pode ser implementado usando o framework Combine, como nós fizemos com o DataProvider no tutorial de MVC.

Presenter e Interactor

Em relação ao interactor, eu implementaria da seguinte forma:

image04
E o presenter seria assim:

image05
image06

Preste atenção às referências fortes e fracas para evitar memory leaks, e veja como o presenter possui métodos para converter o model em ViewModels (contentViewModel(from order: Order) e errorViewModel(from error:)).

Além disso, é possível notar que o interactor lida com lógicas de negócios, decidindo o que deve ser feito e o que deve ser apresentado.

View

Agora, com relação a view, eu seguiria a mesma abordagem dos tutoriais anteriores, substituindo o UIViewController por uma Store, que será o delegate do presenter.

image07image08

Note como a Store apenas envia as ações recebidas para o presenter nos métodos fetchOrder e performAction.

Note também que os métodos render apenas atualizam a propriedade state, cujas mudanças são publicadas usando o framework combine e fazendo a view se recarregar, porque ela possui a Store como objeto observado (observed object).

image09

Coordinator

Agora, para a última parte, nós precisamos implementar o coordinator, que é um pouco complicado e tem diferentes formas de ser feito.

A primeira abordagem possível é fazer algo como a seguir:

image10

Primeiro nós temos um método associatedView, que cria todos os componentes, os conecta, e retorna a view para ser apresentada.

Depois, quando precisamos apresentar outra tela — como a tela de login no exemplo— nós temos que criar o coordinator correspondente, obter sua view e apresentá-la.

Para apresentar a tela inicial, é necessário seguir os mesmos passos no arquivo SceneDelegate.swift:

image11
Note que nós não estamos apresentando a View do SwiftUI direto. Em vez disso, estamos colocando ela em um UIHostingController e colocando o primeiro UIHostingController em um UINavigationController.

Dessa forma, nós temos um UIViewController com um navigation controller e é possível usar métodos como present, push e pop ViewController para controlar a navegação.

Obviamente, essa não é uma abordagem ideal, já que estamos voltando a usar elementos do UIKit ao invés de usar apenas SwiftUI.

O motivo para eu apresentar essa abordagem é que SwiftUI não fornece (até o momento) uma boa maneira de controlar a stack de navegação. A única forma de navegar é usando NavigationLink, tornando difícil para o coordinator apresentar as telas.

No entanto, ainda é possível, como veremos a seguir.

Coordinator (segunda abordagem)

Primeiro, temos que usar um NavigationLink na view no lugar de um Button:

image12
A seguir, precisamos adicionar informações sobre navegação no nosso ViewModel, para que a view possa observá-la e navegar quando uma flag shouldNavigate mudar de false para true. Olhe novamente como o NavigationLink foi criado no bloco de código anterior.

image13

Logo após, o coordinator precisa delegar para a Store a responsabilidade de mudar as informações de navegação do ViewModel, e assim iniciar a navegação:

image14
E, finalmente, a Store precisa implementar o delegate do coordinator:

image15

Comparando as abordagens

A segunda abordagem funciona e utiliza apenas SwiftUI, mas tem algumas desvantagens:

  • A view precisa saber se um botão pode iniciar alguma navegação ou não para usar um Button ou um NavigationLink. Dessa forma, a view precisa ter conhecimento sobre algumas regras de negócio que estão além do seu escopo.
  • Para cada botão que pode iniciar uma navegação, nós precisamos de um ViewModel correspondente indicando se deve navegar ou não.
  • Não há nenhuma forma (que eu conheça) de realizar ações como voltar para a raíz da stack (pop to root) ou para uma view específica (pop to ViewController) usando elementos do SwiftUI (você precisará da abordagem com UIKit para fazer isso).
  • Por último, o NavigationLink usado no exemplo não está funcionando muito bem e possui um bug descrito aqui, mas que provavelmente será corrigido logo.

Eu acredito que a Apple vai fornecer uma maneira melhor de controlar a navegação logo, mas, por enquanto, eu recomendo que você use a primeira abordagem, com coordinators dando push e pop em UIViewControllers.

image16

Passando dados entre telas

Um último ponto é como passar dados entre telas — imagine, por exemplo, que você precise passar a venda para a tela de sucesso. Nesse caso, o método associatedView do SuccessCoordinator poderia receber o objeto order e injetar no interactor:

image17

E o método presentSuccessView seria:

image18

 

Arquitetura final

Resumindo, a arquitetura VIPER usando SwiftUI seria assim:

image19E se usarmos Clean Swift, a integração com SwiftUI precisaria basicamente das mesmas mudanças (substituir o ViewController pela Store, e implementar o Coordinator usando uma das abordagens apresentadas).

A diferença entre as arquiteturas seria como a View, o Presenter e o Interactor iriam se comunicar. Mas isso está relacionado ao design de cada arquitetura e não ao uso de SwiftUI. O resultado final seria:

image20

É isso tudo! Me conta aqui nos comentários como foi sua experiência e se consegui te ajudar!

Charântola

Charântola

iOS developer. Interested in code architecture, algorithms, new technologies, movies and series.

Deixe um comentário

Categorias

Posts relacionados

Siga-nos

Baixe nosso e-book!

%d blogueiros gostam disto: