Antecedentes:
Claro, a escolha de uma abordagem arquitetônica afeta a implementação da navegação e a organização do transporte de dados no projeto, no entanto, a abordagem em si é composta de uma série de circunstâncias: composição da equipe, tempo para o mercado, o estado da especificação técnica, a escalabilidade do projeto e muitos outros, os fatores determinantes para mim foram:
- uso obrigatório de MVVM;
- a capacidade de adicionar rapidamente novas telas (controladores e seus modelos de visualização) ao processo de navegação;
- mudanças na lógica de negócios não devem afetar a navegação;
- as mudanças na navegação não devem afetar a lógica de negócios;
- a capacidade de reutilizar telas rapidamente sem fazer correções na navegação;
- a capacidade de ter uma ideia rápida das telas existentes;
- a capacidade de ter uma ideia rápida das dependências do projeto;
- não aumente o limite para os desenvolvedores entrarem no projeto.
Vá direto ao ponto
Deve-se destacar que a solução final não foi formada em um dia, tem seus inconvenientes e é mais adequada para projetos de pequeno e médio porte. Para maior clareza, o projeto de teste pode ser visto aqui: github.com/ArturRuZ/NavigationDemo
1. Para poder ter uma ideia rápida das telas existentes, decidiu-se criar um enum com o nome inequívoco ControllersList.
enum ControllersList {
case textInputScreen
case textConfirmationScreen
}
2. Por uma série de razões, o projeto não queria usar soluções de terceiros para DI e eu queria obter DI, inclusive com a capacidade de visualizar rapidamente as dependências do projeto, então foi decidido usar Assembly para cada tela separada (fechada pelo protocolo de montagem) e RootAssembly como âmbito geral.
protocol Assembly {
func build() -> UIViewController
}
final class TextInputAssembly: Assembly {
func build() -> UIViewController {
let viewModel = TextInputViewModel()
return TextInputViewController(viewModel: viewModel)
}
}
final class TextConfirmationAssembly: Assembly {
private let text: String
init(text: String) {
self.text = text
}
func build() -> UIViewController {
let viewModel = TextConfirmationViewModel(text: text)
return TextConfirmationViewController(viewModel: viewModel)
}
}
3. Para transferir dados entre telas (onde é realmente necessário) ControllersList transformado em um enum com Valores Associados:
enum ControllersList {
case textInputScreen
case textConfirmationScreen(text: String)
}
4. Para que a lógica de negócios não afetasse a navegação, nem a navegação na lógica de negócios, bem como a rápida reutilização de telas, era necessário mover a navegação para uma camada separada. Assim surgiu o Coordenador e o protocolo de Coordenação:
protocol Coordination {
func show(view: ControllersList, firstPosition: Bool)
func popFromCurrentController()
}
final class Coordinator {
private var navigationController = UINavigationController()
private var factory: ControllerBuilder?
private func navigateWithFirstPositionInStack(to: UIViewController) {
navigationController.viewControllers = [to]
}
private func navigate(to: UIViewController) {
navigationController.pushViewController(to, animated: true)
}
}
extension Coordinator: Coordination {
func popFromCurrentController() {
navigationController.popViewController(animated: true)
}
func show(view: ControllersList, firstPosition: Bool) {
guard let controller = factory?.buildController(for: view) else { return }
firstPosition ? navigateWithFirstPositionInStack(to: controller) : navigate(to: controller)
}
}
É importante notar aqui que o protocolo pode descrever mais métodos, incl. como o Coordenador, pode implementar diferentes protocolos, dependendo das necessidades.
5. Com tudo isso, eu também queria limitar o conjunto de ações que o desenvolvedor deveria executar adicionando uma nova tela ao aplicativo. No momento, era necessário lembrar que em algum lugar é necessário cadastrar dependências, sendo possível realizar algumas outras ações para que a navegação funcione.
6. Eu não queria criar roteadores e coordenadores adicionais. Além disso, a criação de lógica adicional para navegação pode complicar significativamente a percepção da navegação e a reutilização de telas. Tudo isso levou a uma cadeia de mudanças que no final das contas se parecia com esta:
//MARK - Dependences with controllers associations
fileprivate extension ControllersList {
typealias scope = AssemblyServices
var assembly: Assembly {
switch self {
case .textInputScreen:
return TextInputAssembly(coordinator: scope.coordinator)
case .textConfirmationScreen(let text):
return TextConfirmationAssembly(coordinator: scope.coordinator, text: text)
}
}
}
//MARK - Services all time in memory
fileprivate enum AssemblyServices {
static let coordinator: oordinationDependencesRegstration = Coordinator()
static let controllerFactory: ControllerBuilderDependencesRegistration = ControllerFacotry()
}
//MARL: - RootAssembly Implementation
final class RootAssembly {
fileprivate typealias scope = AssemblyServices
private func registerPropertyDependences() {
// this place for propery dependences
}
}
// MARK: - AssemblyDataSource implementation
extension RootAssembly: AssemblyDataSource {
func getAssembly(key: ControllersList) -> Assembly? {
return key.assembly
}
}
Agora, ao criar uma nova tela, bastava o desenvolvedor fazer alterações na ControllersList, para que o próprio compilador mostrasse onde era necessário fazer alterações. Adicionar novas telas ao ControllersList não afetou o esquema de navegação atual de forma alguma, e a lógica de gerenciamento de dependência foi fácil de seguir. Além disso, usando ControllersList, você pode encontrar facilmente todos os pontos de entrada em uma tela específica e agora é fácil reutilizar as telas.
Conclusão
Este exemplo é uma implementação simplificada da ideia e não cobre todos os casos de uso; no entanto, a abordagem em si provou ser bastante flexível e adaptativa.
As desvantagens desta abordagem são as seguintes:
- , , . ControllersList NavigationEvents, , ;
- , ;
- , , . , .
A maioria das postagens sobre navegação e transferência de dados em aplicativos IOS afetam o uso de coordenadores e roteadores (para cada um ou um grupo de telas) ou a navegação por segue, singleton, etc., mas nenhuma dessas opções me serviu para uma ou outra razões.
Talvez esta abordagem seja adequada para resolver problemas, obrigado pelo seu tempo!