Usando Enum + Valores Associados ao Navegar e Transferir Dados entre Telas em Aplicativos IOS

Nesta postagem, gostaria de abordar a velha questão de organizar a navegação e a transferência de dados entre telas em aplicativos IOS. Em primeiro lugar, gostaria de transmitir o conceito da minha abordagem, e não convencê-lo a usá-la como uma pílula mágica. Aqui não vamos considerar várias abordagens arquitetônicas ou a possibilidade de usar UlStoryboard com segues, em geral, vou descrever outra maneira possível de conseguir o que você deseja com seus prós e contras. Então, vamos começar!



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!



All Articles