Problemas de padrão do coordenador e o que o RouteComposer tem a ver com isso

Continuo a série de artigos sobre a biblioteca RouteComposer que usamos e hoje quero falar sobre o padrão do Coordenador. Fui solicitado a escrever este artigo discutindo um dos artigos sobre o padrão, o coordenador aqui em Habré.


O padrão Coordenador, apresentado há não muito tempo, está ganhando cada vez mais popularidade entre os desenvolvedores de iOS e, em geral, é claro o porquê. Porque as ferramentas prontas para uso fornecidas pelo UIKit não são uma bagunça universal.


imagem


Eu já levantei a questão da fragmentação da maneira como componho a visão dos controladores na pilha e, para evitar repetições, basta ler sobre isso aqui .


Sejamos honestos. Em algum momento, Epole percebeu que, ao colocar os controladores no centro de desenvolvimento de aplicativos, ela não ofereceu nenhuma maneira sensata de criar ou transferir dados entre eles e, depois de confiar a solução para esse problema aos desenvolvedores, ele foi preenchido automaticamente pelo Xcode e, talvez, pelos desenvolvedores do UISearchConnroller, em algum momento, introduziu storyboards e segues para nós. Então, Epolus percebeu que ela escrevia aplicativos que consistiam em apenas duas telas e, na iteração seguinte, sugeriu a possibilidade de dividir os storyboards em vários componentes, já que o Xcode começou a falhar quando o storyboard atingiu um determinado tamanho. Os segmentos mudaram junto com esse conceito, em várias iterações que não são muito compatíveis entre si. O apoio deles é fortemente costurado na enorme classe UIViewController e, no final, conseguimos o que conseguimos. Aqui está:


 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "showDetail" { if let indexPath = tableView.indexPathForSelectedRow { let object = objects[indexPath.row] as! NSDate let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController controller.detailItem = object controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem controller.navigationItem.leftItemsSupplementBackButton = true } } } 

O número de transmissões de força neste bloco de código é incrível, assim como as constantes de sequência nos próprios storyboards, para rastrear quais Xcode não oferece nenhum meio. E o menor desejo de mudar alguma coisa no processo de navegação permitirá que você compile o projeto sem nenhum esforço e ele trava com um estrondo no tempo de execução sem o menor aviso do Xcode. Aqui está um WYSIWYG no final, acabou. O que você vê é o que você recebe.


Você pode discutir por um longo tempo sobre os encantos dessas setas cinza nos storyboards supostamente mostrando a alguém as conexões entre as telas, mas, como minha prática demonstrou, entrevistei intencionalmente vários desenvolvedores familiares de empresas diferentes, assim que o projeto cresceu além das 5-6 telas, as pessoas tentaram encontre uma solução mais confiável e, finalmente, comecei a manter a estrutura da pilha de controladores de exibição em minha cabeça. E se o suporte ao iPad e outros modelos de navegação ou o suporte a push fossem adicionados, tudo ficaria triste por lá.


Desde então, várias tentativas foram feitas para resolver esse problema, algumas das quais resultaram em estruturas separadas, outras em padrões arquiteturais separados, desde que a criação de controladores de exibição dentro do controlador de exibição tornou esse código maciço e desajeitado ainda mais.


Vamos voltar ao padrão Coordenador. Por razões óbvias, você não encontrará sua descrição na Wikipedia porque não é um padrão de programação / design padrão. Em vez disso, é um tipo de abstração, que sugere ocultar todo esse código "feio" para criar e inserir uma nova torção do controlador na pilha, salvando links no contêiner do controlador e enviando dados entre os controladores. O artigo mais adequado que descreve esse processo eu chamaria de artigo no raywenderlich.com . Começa a se tornar popular após a conferência NSSpain de 2015, quando o público em geral foi informado sobre isso. Mais detalhadamente, o que foi dito pode ser encontrado aqui e aqui .


Descreverei brevemente em que consiste antes de prosseguir.


O padrão Coordenador em todas as interpretações se encaixa aproximadamente nesta figura:



Ou seja, o coordenador é um protocolo


 protocol Coordinator { func start() } 

E todo o código feio deve estar oculto na função start . Além disso, o coordenador pode ter links para coordenadores filhos, ou seja, eles têm alguma capacidade de composição e, por exemplo, você pode substituir uma implementação por outra. Ou seja, parece muito elegante.


No entanto, as inanidades começam muito em breve:


  1. Algumas implementações propõem transformar o Coordenador de um determinado padrão de geração em algo mais razoável, observando a pilha de controladores e torná-lo um delegado do contêiner , por exemplo, UINavigationController , para processar clicando no botão Voltar ou deslizar para trás e excluir o coordenador filho. Por razões naturais, apenas um objeto pode ser um delegado, o que limita o controle do contêiner e leva ao fato de que essa lógica fica com o coordenador ou cria a necessidade de delegar essa lógica ainda mais a alguém mais abaixo na lista.
  2. Frequentemente, a lógica para criar o próximo controlador depende da lógica de negócios . Por exemplo, para ir para a próxima tela, o usuário deve estar conectado ao sistema. Claramente, este é um processo assíncrono, que inclui a geração de uma tela intermediária com o formulário de login, o próprio processo de login pode terminar com êxito ou não. Para evitar a transformação do coordenador em um coordenador maciço (semelhante ao controlador de visualização maciça), precisamos de decomposição. Na verdade, você precisa criar um Coordenador Coordenador.
  3. Outro problema enfrentado pelos coordenadores é que eles são essencialmente invólucros para controladores de exibição de contêiner, como UINavigationController , UITabBarController e assim por diante. E alguém deve fornecer links para esses controladores . Se com os coordenadores filhos tudo fica ainda menos claro, com os coordenadores iniciais da cadeia, nem tudo é tão simples. Além disso, ao alterar a navegação, por exemplo, para o teste A / B, a refatoração e adaptação de tais coordenadores resulta em uma dor de cabeça separada. Especialmente se o tipo de contêiner mudar.
  4. Tudo isso se torna ainda mais complicado quando o aplicativo começa a suportar eventos externos que geram controladores de exibição. Como notificações por push ou links universais (o usuário clica no link na carta e continua na tela do aplicativo correspondente). Aqui surgem outras incertezas para as quais o padrão do Coordenador não tem uma resposta exata. Você precisa saber exatamente em qual tela o usuário está atualmente para mostrar a ele a próxima tela solicitada por um evento externo.
    O exemplo mais simples é um aplicativo de bate-papo composto por três telas - uma lista de bate-papo, o próprio bate-papo que é empurrado para a navegação do controlador da lista de bate-papo e a tela de configurações exibida de forma modal. O usuário pode estar em uma dessas telas quando recebe uma notificação por push e toca nela. E aqui começa a incerteza, se ele estiver na lista de bate-papo, você precisará iniciar um bate-papo com esse usuário específico, se ele já estiver no bate-papo, será necessário trocá-lo e, se ele já estiver no bate-papo com esse usuário, não faça nada e atualize, se o usuário estiver ligado tela de configurações - aparentemente, você precisa fechar e seguir as etapas anteriores. Ou talvez não feche e mostre apenas o bate-papo sobre as configurações? E se as configurações estão em outra guia, e não modal? Esses if/else começam a se espalhar pelos coordenadores ou a outro mega-coordenador na forma de um espaguete. Além disso, são iterações ativas na pilha de visualizações dos controladores e uma tentativa de determinar onde o usuário está no momento, ou uma tentativa de criar algum tipo de aplicativo que monitora seu estado, mas essa não é uma tarefa fácil, apenas com base na natureza das próprias controladoras de exibição.
  5. E a cereja no bolo são as falhas do UIKit . Um exemplo trivial: um UITabBarController com um UINavigationController na segunda guia com algum outro UIViewController . O usuário na primeira guia causa um determinado evento que requer alternar a guia e UINavigationController outro controlador de exibição para o seu UINavigationController . Tudo isso precisa ser feito exatamente nessa sequência. Se o usuário nunca abriu a segunda guia antes disso e o UINavigationController não UINavigationController chamado no viewDidLoad o método push não funcionará deixando apenas uma mensagem indistinta no console. Ou seja, os coordenadores não podem simplesmente ser ouvintes de eventos neste exemplo; eles devem trabalhar em uma determinada sequência. Então eles devem ter conhecimento um do outro. E isso já contradiz a primeira afirmação do padrão Coordenador, de que os coordenadores não sabem nada sobre os coordenadores geradores e estão conectados apenas aos filhos. E também limita sua permutabilidade.

Essa lista pode ser continuada, mas, em geral, é claro que o padrão do Coordenador é uma solução bastante limitada e pouco escalável. Se você olhar sem óculos cor de rosa, é uma maneira de decompor parte da lógica, que geralmente é escrita dentro de UIViewController maciços, em outra classe. Todas as tentativas de torná-lo mais do que apenas uma fábrica generativa e introduzir outra lógica não terminam bem.


Vale ressaltar que existem bibliotecas baseadas nesse padrão que, de uma maneira ou de outra, permitem mitigar parcialmente as desvantagens acima. Eu mencionaria o XCoordinator e o RxFlow .


O que fizemos?


Tendo participado do projeto que recebemos de outra equipe de suporte e desenvolvimento, com os coordenadores e seu roteador "bisavó" simplificado na abordagem arquitetônica VIPER , voltamos à abordagem que funcionou bem no grande projeto anterior de nossa empresa. Essa abordagem não tem um nome. Está na superfície. Quando tínhamos tempo livre, ele foi compilado em uma biblioteca RouteComposer separada, que substituiu completamente os coordenadores e provou ser mais flexível.


Qual é essa abordagem? Nisso, para confiar na pilha (árvore), torço os controladores como estão. Para não criar entidades desnecessárias que precisam ser monitoradas. Não salve ou rastreie as condições.


Vamos examinar mais de perto as entidades do UIKit e tentar descobrir o que temos na linha de fundo e com o que podemos trabalhar:


  1. A pilha do controlador é uma árvore. Há um controlador de exibição raiz que possui controladores de exibição filho. Os controladores de exibição apresentados modalmente são um caso especial de controladores de exibição filho, pois eles também têm uma ligação com o controlador de exibição gerado. Está tudo disponível imediatamente.
  2. Eu preciso criar entidades de controladores. Todos eles têm construtores diferentes e podem ser criados usando arquivos Xib ou Storyboards. Eles têm diferentes parâmetros de entrada. Mas eles estão unidos porque precisam ser criados. Então, aqui podemos usar o padrão de fábrica , que sabe como criar o controlador de exibição desejado. Cada fábrica é fácil de cobrir com testes de unidade abrangentes e é independente de outras.
  3. Dividimos os controladores de exibição em 2 classes: 1. Basta visualizar os controladores, 2. Controladores de exibição de contêiner (Controller View Controller) . Os controladores de exibição de contêiner diferem dos comuns, pois podem conter controladores de exibição filho - também contêineres ou simples. Esses controladores de exibição estão disponíveis UINavigationController : UINavigationController , UITabBarController e assim por diante, mas também podem ser criados pelo usuário. Se ignorá-lo, podemos encontrar as seguintes propriedades em todos os contêineres: 1. Eles têm uma lista de todos os controladores que eles contêm. 2. Um ou mais controladores estão atualmente visíveis. 3. Eles podem ser solicitados a tornar um desses controladores visíveis. Isso é tudo o que os controladores UIKit podem fazer . Eles apenas têm métodos diferentes para isso. Mas existem apenas 3 tarefas.
  4. Para incorporar um controlador de exibição criado de fábrica, o método de exibição pai do controlador é UINavigationController.pushViewController(...) , UITabBarController.selectedViewController = ... , UIViewController.present(...) e assim por diante. Você pode observar que 2 controladores de exibição são sempre necessários, um já na pilha e um que precisa ser incorporado na pilha. Enrole isso em um invólucro e chame-o de Ação (Ação) . Cada ação é fácil de cobrir com testes de unidade abrangentes e cada um é independente dos outros.
  5. Do exposto acima, verifica-se que, usando entidades preparadas, você pode construir uma cadeia de configuração Fábrica -> Ação -> Fábrica -> Ação -> Fábrica e , após concluir, pode criar uma árvore de visualização de controladores de qualquer complexidade. Você só precisa especificar o ponto de entrada. Esses pontos de entrada geralmente são o rootViewController de propriedade do UIWindow ou o atual controlador de exibição, que é o ramo mais extremo da árvore. Ou seja, essa configuração está escrita corretamente como: Iniciando o ViewController -> Action -> Factory -> ... -> Factory .
  6. Além da configuração, você precisará de alguma entidade que saiba como iniciar e criar a configuração fornecida. Vamos chamá-lo de roteador . Não possui um estado, não possui nenhum link. Possui um método para o qual a configuração é passada e executa sequencialmente as etapas de configuração.
  7. Adicione responsabilidade ao roteador adicionando classes Interceptors à cadeia de configuração. É possível interceptar três tipos: 1. Lançado antes de iniciar a navegação. Removemos as tarefas de autenticação do usuário no sistema e outras tarefas assíncronas neles. 2. Execute no momento da criação do controlador de exibição para definir os valores. 3. Realizado após a navegação e executando várias tarefas analíticas. Cada entidade é facilmente coberta por testes de unidade e não sabe como será usada na configuração. Ela tem apenas uma responsabilidade e a cumpre. Ou seja, a configuração para navegação complexa pode se parecer com [Tarefa de pré-navegação ...] -> Iniciando o ViewController -> Ação -> (Factory + [ContextTask ...]) -> ... -> (Factory + [ContextTask ...]) -> [Post NavigationTask ...] Ou seja, todas as tarefas serão executadas pelo roteador sequencialmente, executando, por sua vez, entidades atômicas pequenas e de fácil leitura.
  8. A última tarefa que não pode ser resolvida pela configuração permanece - este é o estado do aplicativo no momento. E se precisarmos construir não toda a cadeia de configuração, mas apenas parte dela, porque o usuário a passou parcialmente? Essa pergunta sempre pode ser respondida sem ambiguidade pelos controladores da árvore de visão. Porque se parte da cadeia já estiver construída, ela já estará na árvore. Isso significa que, se cada fábrica da cadeia puder responder à questão de ser construída ou não, o roteador poderá entender qual parte da cadeia precisa ser concluída. Obviamente, essa não é uma tarefa da fábrica; portanto, é introduzida outra entidade atômica - o Finder, e qualquer configuração é semelhante a esta: [Tarefa de pré-navegação ...] -> Iniciando o ViewController -> Ação -> (Finder / Factory + [ContextTask ...]) -> ... -> (Localizador / Fábrica + [ContextTask ...]) -> [Post NavigationTask ...] . Se o roteador começar a lê-lo a partir do final, um dos Localizadores dirá que ele já foi construído, e o roteador a partir deste ponto começará a reconstruir a cadeia. Se nenhum deles se encontrar na árvore, será necessário construir a cadeia inteira a partir do controlador inicial.
    imagem
  9. A configuração deve ser fortemente digitada. Portanto, cada entidade trabalha com apenas um tipo de visualização do controlador; um tipo de dados e configuração depende inteiramente da capacidade do Swift de trabalhar com tipos associados . Queremos confiar no compilador, não no tempo de execução. Um desenvolvedor pode intencionalmente enfraquecer a digitação, mas não o contrário.

Um exemplo dessa configuração:


 let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) .add(LoginInterceptor<UUID>()) // Have to specify the context type till https://bugs.swift.org/browse/SR-8719 is fixed .add(ProductViewControllerContextTask()) .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) .using(UINavigationController.push()) .from(NavigationControllerStep()) .using(GeneralActions.presentModally()) .from(GeneralStep.current()) .assemble() 

Os itens descritos acima cobrem toda a biblioteca e descrevem a abordagem. Tudo o que resta para nós é fornecer as configurações de cadeia que o roteador executará quando o usuário clicar em um botão ou ocorrer um evento externo. Se houver tipos diferentes de dispositivos, por exemplo, iPhone ou iPad, forneceremos diferentes configurações de transição usando o polimorfismo. Se tivermos testes A / B, a mesma coisa. Não precisamos pensar no estado do aplicativo no momento de iniciar a navegação, precisamos garantir que a configuração seja escrita corretamente inicialmente e temos certeza de que o roteador a construirá de alguma forma.


A abordagem descrita é mais complicada do que uma certa abstração ou padrão, mas ainda não enfrentamos o problema em que não seria suficiente. Obviamente, o RouteComposer requer algum estudo e entendimento de como funciona. No entanto, isso é muito mais fácil do que aprender o básico do AutoLayout ou RunLoop. Sem matemática mais alta.


A biblioteca, assim como a implementação do roteador fornecido, não usa truques objetivos com o tempo de execução e segue totalmente todos os conceitos do Cocoa Touch, apenas ajudando a dividir o processo de composição em etapas e executá-los na sequência especificada. A biblioteca é testada nas versões 9 a 12 do iOS.


Mais detalhes podem ser encontrados em artigos anteriores:
Composição dos controladores UIViewControllers e navegação entre eles (e não apenas) / revista geek
Exemplos de configuração de UIViewControllers usando o RouteComposer / geek magazine


Obrigado pela atenção. Ficarei feliz em responder perguntas nos comentários.

Source: https://habr.com/ru/post/pt446550/


All Articles