Coordenando Aplicativo iOS Modularizado

Brenno de Moura
6 min readAug 14, 2021

A modularização de aplicativos tem uma série de benefícios e desafios. Uma boa modularização explora o recurso do Xcode de compilação em paralelo dos arquivos do projeto. Quando temos muitas dependências, como o módulo do Firebase que consome um tempo considerável de build, o tempo de compilação aumenta drasticamente.

Pensando nisso, a modularização tem o objetivo não só de tornar a compilação do projeto independente do Firebase, ou qualquer outro módulo, mas também de organizar a forma como os desenvolvedores trabalham. Além disso, ela permite a fragmentação do time por módulos.

Este artigo se concentra em implementar uma estrutura que permita usar os conceitos de modularização nos aplicativos que usam Coordinator. No entanto, essa estrutura pode ser adaptada para outras arquiteturas, uma vez que a proposta de modularização se concentra na transição das telas, mantendo uma complexidade simples de implementação e utilização.

A concepção desse artigo ocorreu após longos estudos sobre modularização e em como encaixá-la no processo de desenvolvimento de maneira simples. Na comunidade não se discute de forma clara sobre esse assunto e, muito menos, em um processo de implementação objetivo.

Alguns projetos misturam vários outros conceitos o que dificulta a compreensão do assunto. No entanto, inspirado em diversos artigos sobre modularização, este documento irá detalhar o processo para tornar qualquer aplicativo modularizado.

Antes de entrar na implementação, o ponto central para a compreensão do problema é que os aplicativos contêm funções específicas, detalhando alguma informação para o usuário.

Pensando em peças de quebra cabeças, temos uma peça para o login do usuário, outra peça para o perfil, outra para o carrinho de compras e assim por diante. Em alguns artigos iniciantes sobre modularização, é utilizado o conceito de features que especifica as partes do aplicativo.

Ao tentar separar as peças, no caso de um aplicativo com suporte ao conceito de Coordinator, surge o problema da conexão entre as telas. Como a HomeViewController (HomeFeature) irá se comunicar com a ProfileViewController (ProfileFeature)? Além disso, como fica a apresentação das telas no UINavigationController, ou UITabBarController, ou na própria UIViewController? Essas foram as duas perguntas mais difíceis de compreender para obter alguma solução.

Coordinator

Falando brevemente sobre o Coordinator, eles tem como responsabilidade fazer as transições entre as telas. Cada view controller implementada, deve ter acesso ao seu coordinator, com as regras de transição para as demais telas.

Para não aprofundar nesse assunto, que não é o objetivo desse artigo, no HomeCoordinator existe a função showProfile que se comunica com o ProfileCoordinator, fazendo a transição definida por ele. Uma boa construção dos coordinators facilitará o processo de modularização.

Swift Package Manager

O Swift Package Manager (SPM) é um gerenciador de dependencias da Apple que distribui as bibliotecas, frameworks e módulos em repositórios com suporte ao git. É integrado ao Xcode e permite a criação rápida com o comando swift package init , além da codificação diretamente pela IDE ao abrir o Package.swift.

Possui um arquivo de configuração simples e com vários recursos importantes, principalmente após o Swift 5.3. É recomendado uma leitura sobre esse tópico, no entanto não é obrigatório para este artigo, pois podem ser utilizado outras formas de distribuição e modularização como o xcframework ou cocoapods.

NotificationCenter

O NotificationCenter é fundamental para a implementação da solução de modularização. Ele faz a comunicação entre módulo (Feature) e o aplicativo para fazer as transições de tela.

Podem ser exploradas outras soluções para realizar esse processo, porém neste artigo foi utilizado o NotificationCenter e a propriedade estática shared que é visível tanto para os módulos quanto para a aplicação. Podem ser feitas abstrações para tornar o processo de notificações privado, criando uma outra instância do NotificationCenter, além de uma API de métodos focados nesse processo.

FlowType

O FlowType é um protocolo usado para enviar os pedidos de transição de tela dos módulos para a aplicação, onde cada módulo deve implementar o seu Flows, como o HomeFlows (HomeFeature), por exemplo. Conforme a Figura 1, é detalhado o protocolo FlowType sem métodos obrigatórios e nem propriedades.

Ele não possui dependência com outros protocolos, como o RawRepresentable, mantendo o protocolo primitivo e apto para ser utilizado sem tipos genéricos. No entanto, os objetos que irão implementá-lo devem ser enumeradores, onde cada caso pode ser usado para enviar dados para as próximas telas, como case profile(User) ou case login .

Figura 1 — Definição do protocolo FlowType.

A utilização desse protocolo no NotificationCenter pode ser direta, usando os métodos post e addObservable. No entanto, para garantir a integridade e um processo mais simples de codificação, é realizado uma abstração para dar suporte o protocolo. Com isso, a API para fazer a transições de tela fica objetiva.

A Figura 2 detalha a implementação dos dois métodos post(_ rootViewController: UIViewController, _ flow: FlowType) e track(perform action: @escaping (UIViewController, FlowType) -> Void). O primeiro método é utilizado pelo coordinator para submeter o pedido de transição para a tela desejada, e o segundo método é usado pela aplicação para tratar o pedido e chamar o coordinator correspondente.

Figura 2 — Extensão da NotificationCenter com os método post(_:_:) e track(perform:).

HomeCoordinator

O HomeCoordinator é o coordinator de exemplo que contém os métodos showProfile(_ user: User) e o showLogin caso o usuário tenha feito o logout. O primeiro passo é definir o HomeFlows, conforme a Figura 3, que conta com os dois casos necessários para permitir a transição da HomeViewController para as demais. O segundo passo é utilizar o método post(_:_:) implementado diretamente no NotificationCenter para enviar o pedido de transição.

Figura 3 — Definição do HomeFlows.

O HomeCoordinator utiliza a UINavigationController para fazer as transições de tela dentro do módulo. Normalmente, a tela inicial não possui outras telas vinculadas no fluxo, sendo principalmente usada para iniciar outros fluxos (comunicando com outras features).

No entanto, no caso de um fluxo como de perfil, podem existir várias telas dentro do módulo e consequentemente vários coordinators com comunicação direta entre eles, sem passar pelo uso do FlowType e dos métodos de transição via NotificationCenter.

É importante ressaltar que os métodos discutidos são usados apenas para comunicação entre os módulos, intermediado pela aplicação.

A Figura 4 detalha a implementação dos métodos do HomeCoordinator utilizando o FlowType. No caso da feature de perfil, o ProfileCoordinator utiliza a UINavigationController para fazer a transição entre a tela inicial e a tela do perfil.

Já o login é feito a substituição da tela atual pela tela de login, podendo fazer uso ou não da navigation controller. Apesar disso, essa decisão deve ser tomada pela aplicação que fará a transição entre as telas e, por isso, os coordinators sempre informam a view controller que será usada na transição das telas.

Figura 4 — Implementação das funções de transição no HomeCoordinator.

AppDependencies

O AppDependencies é um objeto instanciado pela aplicação no AppDelegate ou em outro objeto que trata da integridade do aplicativo. Ele possui o método trackDependencies que é chamado no init.

A preferência de utilizar instância ou métodos estáticos deve ser decidida para melhor encaixar ao projeto. Utilizando o método track do NotificationCenter, podemos fazer a conversão dos tipos para os FlowType desejado e realizar as operações de transição desejada.

A Figura 5 implementa o método trackDependencies e chama o homeFlow(_ rootViewController: UIViewController, _ flow: HomeFlows) para que a chamada dos coordinators responsáveis seja feita. No caso do login, essa transição é feita pelo AppDelegate, através do método switchToLogin().

Já o perfil, o AppDependencies instância o ProfileCoordinator passando a UINavigationController para que a transição seja feita corretamente por ele, conforme esperado. Dessa forma, é possível utilizar essa solução para realizar diversos tipos de operações, mantendo a independência dos módulos e a responsabilidade do aplicativo em fazer as transições.

Figura 5 — Implementação dos métodos trackDependencies e homeFlow(_:_:).

Considerações

A estrutura discutida nesse artigo permitiu a implementação de um aplicativo com 20 features diferentes com sucesso, mantendo a integridade da aplicação. Ela cobre transições como de push, present e outras mais complexas que envolvem outras partes da aplicação, principalmente em SwiftUI, como um AppState.

Em termos gerais, as transições perderam a inteligência para verificar em qual parte do fluxo a aplicação se encontra. Apesar disso ser um ponto fraco, não foi encontrado nenhum problema referente a isso.

A API dos módulos que definem os métodos e objetos visíveis, marcados com o atributo public, também é simplificada. Nos módulos apenas são públicos os coordinators que podem ser interagidos externamente e o FlowType implementado no módulo.

Na maioria deles, é possível que o módulo tenha visivelmente apenas o coordinator, o método de inicialização, o start e o flow. Dessa forma, essa abstração tem vários pontos positivos a serem considerados e pode ser o início para os programadores conhecerem e utilizarem os conceitos de modularização nos aplicativos.

Obrigado por chegar até aqui!

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.

--

--

Brenno de Moura
Brenno de Moura

Written by Brenno de Moura

Software engineer with a passion for technology and a focus on declarative programming, experience in challenging projects and multidisciplinary teams

Responses (2)