iOS MVP-C Modularizado
A modularização de aplicativos iOS está sendo cada vez mais exigido pelas empresas e também ao escalonamento dos projetos, devido a sua otimização de gerenciamento de equipes como também do build time. Modularizar um aplicativo envolve muitos outros pontos que precisam ser trabalhados para terem um excelente efeito na arquitetura do projeto, no entanto, este artigo detalha apenas a parte de UI (View, ViewController, Presenter e Coordinator) comum em todos os aplicativos.
Existem vários artigos publicados em minha página que abstraem alguma implementação e abordagem específica sobre a View ou Coordinator, somando como bagagem para ajudar a montar estruturas mais complexas em um aplicativo. No entanto, a leitura destes artigos não é obrigatória e não deve impactar na compreensão deste.
Detalhar a modularização de um aplicativo é bastante complexo e fácil de perder o raciocínio durante a concepção e, por isso, ainda não havia me disposto a isso aqui no Medium. Tive contato com alguns projetos, artigos e implementações que abordam vários conceitos de forma superficial e confusa e, espero, que este artigo siga uma direção oposta, permitindo implementar vários projetos usando arquitetura modular.
Confesso que não conheço a maioria dos assuntos que envolve esta implementação e posso quebrar algum princípio, como o SOLID, Clean Architecture ou outros. Dessa forma, seja bem vindo para trazer essa discussão para a seção de comentários, onde podemos compreender melhor sobre este tema e conhecer outros pontos de vista.
Conceitos
Modularização nada mais é que dividir um aplicativo em módulos específicos. Cada módulo deve ter um propósito e acessar pedaços da aplicação para conseguir realizar as operações de UI, como também de requisições, validação, acesso ao banco de dados e outros.
É muito indicado para este assunto estudar e absorver os conteúdos na internet sobre Clean Architecture, Domain Driven Design e outros links que estão disponível na web e também no Médium. No entanto, os conceitos abordados neste artigo estão relacionado a modularização da interface de usuário.
Todo aplicativo iOS contém uma camada de UI que apresenta informações e interage com o usuário, realizando transições e animações na interface. Com isso, os módulos devem obedecer uma nomenclatura para padronizar e contextualizar as partes, sendo recomendado utilizar o formato Nome + Feature formando módulos chamados LoginFeature, UserFeature e outros.
Existem várias formas de implementar essa camada e, para este artigo, é utilizado a arquitetura MVP-C onde temos o Model, a View (ViewController / View), Presenter (ou ViewModel não reativa) e o Coordinator.
Protocolos
A implementação de uma feature modularizada envolve a definição de alguns protocolos para cada objeto, não sendo obrigatório, importante para implementar os testes unitários dos objetos e generalizar soluções. Além disso, o uso de protocolos limita o uso de métodos e propriedades dos objetos para aqueles em específico não se importando para quem os implementam ou as operações que eles realizam.
Cada componente da arquitetura MVP-C deve receber o seu protocolo abstrato para que seja usado sem conhecer o objeto concreto. Os protocolos são o Viewable, Presenting (ou ViewModelling) e Coordinating.
Cada módulo deve conter a pasta Scenes com as subpastas por tela. As tela devem conter no mínimo quatro pastas: Coordinators, Presenters, Views, ViewControllers e Protocols. Conforme a Figura 1, cada pasta deve conter o mínimo de arquivos possível, mas pode acontecer, raramente, de existem subviews implementadas separadas e que devem ser colocadas dentro da pasta Views.
Um outro ponto importante sobre os protocolos, e a correspondência deles com a arquitetura dentro da pasta Scenes, é que nem sempre uma subpasta está definindo uma tela. Essa estrutura pode ser explorada para definir subpastas específicas para células ou outras views, não necessariamente precisando de ter todos os quatro objetos da arquitetura MVP-C.
View Code
O uso de view code para este artigo é essencial, enquanto o de storyboard ou xib deve ser abolido. Projetos que estão estruturados em storyboards ou xib apresentam uma complexidade muito grande para adotar esse estilo de modularização, sendo totalmente necessário fazer a transcrição da ViewController em storyboard para código em Swift.
O estilo de modularização via protocolo e definição restrita dos objetos da arquitetura permitem tornar a View um componente renderizado pelo SwiftUI, onde a Presenter é do tipo ObservableObject. Para isso, é necessário utilizar tipos genéricos para a View e ViewController que representem o Presenter e assim estar em conformidade com as regras do Swift em relação ao tipo associado.
Uma boa implementação em view code remove o uso direto de constraints nativas ou de bibliotecas, exigindo apenas o uso das constraints de margem com constante ou offset zero. No caso do SwiftUI isso vem naturalmente por conta do uso da VStack, HStack e ZStack. Para o UIKit, é necessário explorar de forma obrigatória o uso da UIStackView.
FlowType
O FlowType é protocolo que permite a comunicação entre os Coordinators em módulos diferentes intermediado pelo App. No entanto, devem existir outras formas de integrar esses componentes usando outras estruturas que permitem a instanciação do Presenter, da ViewController e do Coordinator com propriedades constantes ou não opcionais.
No contexto de modularização, podem ser encontrados o uso de delegates entre os Coordinators ou entre a View e a ViewController. Entretanto, essa abordagem pode aumentar a complexidade do projeto além de não se encaixar com as abstrações do SwiftUI.
Há um artigo que detalha o uso do FlowType pelos Coordinators e o quanto autônomo ele é para levar a diferentes fluxos. Caso tenha interesse de conhecer essa abordagem, o link se encontra logo a baixo.
O FlowType pode ser substitudo por um Router ou inspirar a implementação de uma estrutura ainda mais complexa. Sua concepção simplificada e específica surgiu para encaixar com o problema da comunicação entre os Coordinators e permitir que as transições entre as telas fossem feitas.
View
A View representada pela UIView do UIKit ou pelo protocolo View do SwiftUI é onde todas as operações de layout ocorrem. Ela possui uma autonomia sobre o seu conteúdo não dependendo da existência da Controller.
No entanto, a View implementa o protocolo Viewable respectivo apenas quando no contexto de componente único. Quando ela é definida para renderizar uma tela, o protocolo Viewable deve ser implementado pela Controller.
Essa diferenciação é importante devido as operações técnicas que podem ser realizadas sobre cada componente. Quando no contexto de tela, pode ser necessário acessar propriedades exclusivas da UIViewController pela presenter. Já no contexto de componente, a comunicação fica restrita ao componente gráfico da view como no caso da UITableViewCell.
A View e a Controller devem ser tratadas como objetos semelhantes respeitando suas diferenças.
Tanto a View e quanto a ViewController compartilham o presenter e podem acessar propriedades ou métodos diretamente. Conforme a Figura 2, isso agiliza o processo de view code e também é útil para a integração do presenter com a view, mantendo a comunicação direta e sem o uso de um terceiro componente ou protocolo para intermediar o acesso a propriedades e chamadas de métodos.
A Controller deve ser capaz de chamar os métodos da View encapsulada para informar quando uma lista carregou ou quando alguma condição alterou. Por isso, apesar da View no contexto de tela não implementar o protocolo Viewable, é comum, na maioria dos casos, que ela tenha os métodos que a Controller implementa devido ao protocolo.
Conforme apresentado na definicação de view code, é essencial utilizar esse padrão de desenvolvimento do layout usando código. Para potencializar esse processo, recomendo a leitura do segundo artigo logo a baixo que define como tornar o processo de view code declarativo e semelhante ao SwiftUI.
A Figura 3 mostra a implementação da View usando o SwiftUI e o Presenter como tipo genérico. Essa abordagem permite unir o melhor dos dois mundos, uma vez que todo o layout é renderizado pelo novo framework da Apple e as Controllers de navegação ou de tab é mantido no estilo antigo do UIKit.
Dessa forma, ao padronizar a implementação da View em view code e, em SwiftUI, passando o presenter diretamente, há um controle maior entre esses objetos que possuem uma comunicação frequente seja por interação do usuário ou eventos de outras camadas. Além disso, o uso direto permite uma performace de programação mais rápida, uma vez que a dependência em manter um terceiro protocolo de delegate atualizado é removida.
ViewController
A ViewController neste contexto de modularização é ainda mais limitada para realizar operações de layout específicas e montar a hierarquia de views por meio do método loadView
. Apesar disso, ela tem um papel muito importante para o UIKit e podem existir operações que devem ser realizadas a partir dela.
O seu uso nem mesmo é removido no caso de um aplicativo modularizado que utiliza o SwiftUI para renderizar a view. Ela permite que seja utilizado a UINavigationController e a UITabViewController com o navigationItem e tabBarItem.
A UIViewController fornece uma API consistente que ainda é essencial para complementar os projetos.
Em alguns exemplos detalhados em artigos de outros autores, no caso do SwiftUI, é removido por completo o uso da UIViewController sendo utilizado a UIHostingController diretamente no Coordinator. É uma opção válida, mas não se encaixa em todos os cenários onde a instância da UIViewController pode ser exigida de forma direta.
Em uma aplicação em UIKit apenas, a UIViewController fornece uma API íntegra com um ciclo de vida consistente que é explorado por diversos aplicativos para carregar listas e disparar eventos para alterar o layout. Também, ao manter esta abordagem, a Controller passa a ter acesso tanto ao Presenter e à View diretamente.
Conforme a Figura 4, é detalhado o exemplo da UserDetailViewController implementando o protocolo UserDetailViewable para receber atualizações de dados geradas pela Presenter. Por conta da Presenter ser compartilhada, a Controller assume responsabilidades limitadas e específicas sendo um objeto que intermedia a comunicação da Presenter para a View.
No caso da Controller encapsular uma View implementada usando o SwiftUI, é necessário implementar uma view que transforme a UIViewController em UIView. Assim, conforme a Figura 5, ao criar a UIHostingController no loadView
é chamado o método hostingInView sendo atribuida a view da Controller.
Quanto mais o SwiftUI avança em abstrair as implementações do UIKit, menos dependente os projetos ficarão da UIViewController permitindo de fato remover esse objeto por completo. No entanto, a estabilidade dada ao usar a Controller para construir as telas ainda é essencial.
Por recomendação, logo a baixo está um terceiro artigo que detalha a implementação de um Design System para cor, espaçamento, fonte e componentes para os projetos. Além disso, também é discutido o desenvolvimento de métodos utilitários (Componentes Extras) para criar uma API de view code declarativa.
Presenter
O Presenter é um objeto que concentra todas as operações para obter dados e atualizar as informações na View. Enquanto a View e Controller atuam como uma lataria de um carro, o Presenter é o capô onde se concentra todas as engrenagens que faz o carro funcionar.
Em sua implementação mais simples e primitiva, o Presenter contém uma variedade de código para obter dados, requisitar permissões, mostrar erros e atualizar o banco de dados.
Quando falamos de modularização, é possível quebrar o Presenter em vários UseCases que ficam responsáveis por uma pequena parte do todo, como uma engrenagem. É comum nos projetos modularizados, encontrar um UseCase para validar o campo e-mail, outro para obter os dados de alguma API e outro para autenticar o usuário com o Face ID.
Essas abstrações e especializações de código em UseCase é uma excelente alternativa que mantém o Presenter com poucas responsabilidades, enquanto suas funções (validação de e-mail e outros) são compartilhados entre vários UseCases. A Figura 6 mostra a implementação do LoginPresenter, fazendo uso do EmailValidatorUseCase e do FaceIDAuthenticatorUseCase.
Este artigo não explora o uso de injeção de dependência, porém o seu uso para armazenar e instanciar os UseCases pode ser uma alternativa válida que auxilia no compartilhamento de código entre diferentes Features.
Com o SwiftUI essa implementação altera devido a ideia de que a View é representada por estados e, mesmo o presenter, acaba sendo obrigado armazenar e refletir os dados para a View. A Figura 7 mostra como seria essa mesma implementação em SwiftUI.
Um ponto interessante nesse caso é que o protocolo Viewable acaba perdendo algumas funções para notificar a View que algo mudou. Como o SwiftUI conhece o estado da Presenter, quando ele altera, automaticamente a View é atualizada com os novos valores.
Além disso, o protocolo Viewable acaba sendo utilizado para apenas notificar a Controller de algum evento para chamar algum método exclusivo do UIKit ou de frameworks legados. Essa implementação acaba facilitando para Views que contêm listas ou, caso seja necessário, implementar uma unidade de View para que seja acoplada em outra Feature ou tela.
Coordinator
O Coordinator é um objeto responsável por gerenciar o fluxo de telas. Na comunidade iOS, esse objeto pode ser implementado de formas diferentes e possuir propriedades weak
ou apenas var
dependendo do entendimento da equipe.
A implementação padrão do Coordinator, e mais conhecida, é pelo contexto de fluxo. Em um aplicativo, temos vários fluxos que o usuário pode percorrer, seja para editar várias informações do perfil, ou para comprar um produto ou para efetuar o login.
No caso do fluxo para efetuar o login, podemos encontrar o LoginCoordinator dentro da Feature de login com todas as opções para avançar no fluxo, seja para abrir a tela de cadastro, ou recuperar a senha e demais telas que compõe o fluxo. Com isso, o Coordinator é compartilhado entre vários Presenters e acaba possuindo uma quantidade de código maior que 500 linhas com métodos que sobrepõe uns aos outros.
Essa abordagem ainda explora outros conceitos relacionados a implementação de Coordinator que acaba tornando o objeto para transitar entre telas bastante complexo.
A solução apresentada neste artigo envolve a implementação de um Coordinator limpo com poucos métodos, apenas para chamar outros Coordinators, e para montar a Controller na tela do dispositivo. Essa implementação é totalmente válida para somar com injeção de dependência e também para montar fragmentos de View + Presenter.
Para as transições ocorrerem, é necessário um hospedeiro para realizar a animação e substituição de uma Controller para a outra. Temos a UINavigationController, a UITabBarController, a UIViewController e a UIWindow.
O hospedeiro é referenciado dentro do Coordinator pela weak var rootViewController
ou weak var window
sendo configurado pelo init. Além disso, todo Coordinator possui a função start()
especificado pelo protocolo Coordinator
. A Figura 8 mostra o LoginCoordinator que exemplifica essa implementação.
Com essa implementação, podemos adicionar variáveis e passar por parâmetro na hora de chamar um Coordinator da próxima tela, mantendo os dados não opcionais quando necessário. Também, por conta da View em SwiftUI ser renderizada na UIViewController e assim por tela, podemos utilizar esse mesmo código para a solução declarativa da Apple.
Um outro ponto interessante é utilizar o FlowType para realizar transições de tela entre Features diferentes. Ele torna o processo de codificação ainda mais simples e permite o direcionamento dos fluxos dinamicamente pela aplicação.
No caso de um Coordinator para uma View em SwiftUI, podemos combinar essa implementação com o Coordinator.Provider
ou Coordinator.Builder
que também é uma View. Essa implementação é recomendada para quando temos uma View que é usada em diferentes telas e Feature, e queremos manter ela única e reutilizável.
A Figura 9 mostra como é a implementação de um Coordinator para a View do SwiftUI. Podemos usar o objeto Provider
para adicionar a UserView em qualquer tela. A integração desse objeto com as telas envolve alguns ajustes e código para gerar o resultado desejado.
Considerações
A implementação da arquitetura MVP-C em aplicativos iOS envolve uma série de conceitos e opções. O que é apresentado neste artigo é mais uma opção para ser discutida e conhecida pelos desenvolvedores.
Devido o compartilhamento do Presenter entre View e Controller (talvez o nome correto até seria ViewModel 🤷🏻♂️) auxilia na implementação dos objetos de UI sem exigir um outro protocolo para complementar o diálogo entre esses dois objetos. A inversão do conceito do Coordinator, por tela e com parâmetros necessários no Presenter passados diretamente na sua inicialização, torna os dados mais seguros e garantidos evitando o uso desnecessário de if let
ou guard let
.
Recomendo estudos sobre UseCases, modularização das demais camadas e injeção de dependência para complementar o que foi discutido neste artigo, possibilitando a implementação completa de um aplicativo.
Espero que esse conteúdo tenha sido relevante e a seção de comentários fica aberta para trocarmos ideia e compartilhar experiências!
Muito obrigado pela leitura!
Se quiser contribuir para que eu possa continuar produzindo mais conteúdos técnicos, sinta-se à vontade para me oferecer um café ☕️ através da plataforma Buy me a Coffee.
Seu apoio é fundamental para manter meu trabalho e contribuir com a comunidade de desenvolvimento.