Exemplos de configuração de UIViewControllers usando RouteComposer

Em um artigo anterior , falei sobre a abordagem que usamos para compor e navegar entre os controladores de exibição em vários aplicativos em que trabalho, o que, como resultado, resultou em uma biblioteca RouteComposer separada. Recebi uma quantidade significativa de comentários agradáveis ​​sobre o artigo anterior e alguns conselhos práticos, o que me levou a escrever outro que explicasse um pouco mais sobre como configurar a biblioteca. Sob o corte, tentarei fazer algumas das configurações mais comuns.



Como o roteador analisa a configuração


Para começar, considere como o roteador analisa a configuração que você escreveu. Veja o exemplo do artigo anterior:


let productScreen = StepAssembly(finder: ClassFinder(options: [.current, .visible]), factory: ProductViewControllerFactory()) .using(UINavigationController.pushToNavigation()) .from(SingleContainerStep(finder: NilFinder(), factory: NavigationControllerFactory())) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() 

O roteador passará pela cadeia de etapas, começando desde o primeiro, até que uma das etapas (usando o Finder fornecido) notifique que o UIViewController desejado já UIViewController na pilha. (Por exemplo, GeneralStep.current() garantido que GeneralStep.current() presente na pilha do controlador.) Em seguida, o roteador começará a recuar ao longo da cadeia de etapas, criando os UIViewController necessários usando os UIViewController fornecidos e integrando-os usando as Action especificadas. Graças à verificação de tipo, mesmo no estágio de compilação, na maioria das vezes, você não poderá usar UITabBarController.addTab incompatíveis com o Fabric fornecido (ou seja, não poderá usar UITabBarController.addTab no controlador de UITabBarController.addTab criado por NavigationControllerFactory ).


Se você imaginar a configuração descrita acima, se tiver apenas um determinado ProductViewController na tela, as seguintes etapas serão executadas:


  1. ClassFinder não encontrará o ProductViewController e o roteador continuará
  2. NilFinder nunca encontrará nada e o roteador seguirá em frente
  3. GeneralStep.current sempre retornará o UIViewController mais alto da pilha.
  4. Se o UIViewController encontrado, o roteador retornará
  5. Cria um UINavigationController usando o `NavigationControllerFactory
  6. Vai mostrá-lo modalmente usando GeneralAction.presentModally
  7. ProductViewController ProductViewController ProductViewControllerFactory
  8. Integra o ProductViewController criado ao UINavigationController anterior usando UINavigationController.pushToNavigation
  9. Terminar navegação

NB: Deve-se entender que, na realidade, é impossível mostrar um UINavigationController sem algum UIViewController dentro dele. Portanto, as etapas 5 a 8 serão executadas pelo roteador em uma ordem ligeiramente diferente. Mas você não deve pensar sobre isso. A configuração é descrita sequencialmente.


Uma boa prática ao escrever uma configuração é presumir que o usuário possa estar localizado em qualquer lugar do seu aplicativo no momento e, de repente, receber uma mensagem push com uma solicitação para chegar à tela que você está descrevendo e tentar responder à pergunta - "Como o aplicativo deve se comportar? ? "," Como o Finder se comportará na configuração que estou descrevendo? ". Se todas essas perguntas forem levadas em consideração, você obtém uma configuração que garante ao usuário a tela desejada onde quer que ele esteja. E este é o principal requisito para aplicativos modernos das equipes envolvidas no marketing e na atração de usuários.


StackIteratingFinder e suas opções:


Você pode implementar o conceito do Finder da maneira que achar mais aceitável. No entanto, o mais fácil é percorrer o gráfico dos controladores de exibição na tela. Para simplificar esse objetivo, a biblioteca fornece StackIteratingFinder e várias implementações que executarão essa tarefa. Você só precisa responder à pergunta - este é o UIViewController que você espera.


Para influenciar o comportamento do StackIteratingFinder e informar em quais partes do gráfico (pilha) quero que os controladores procurem, você pode especificar uma combinação de SearchOptions ao criá-lo. E eles devem morar com mais detalhes:


  • current : O controlador de exibição mais alto na pilha. (O que é o rootViewController UIWindow ou o que é mostrado modalmente no topo)
  • visible : se o UIViewController for um contêiner, procure em seu UIViewController visível (por exemplo: UINavigationController sempre possui um UIViewController visível, o UISplitController pode ter um ou dois, dependendo de como é apresentado.)
  • contained : No caso de o UIViewController ser um contêiner, procure em todos os seus UIViewController aninhados (por exemplo: Percorra todos os controladores de exibição do UINavigationController incluindo o visível)
  • presenting : pesquise também em todos os UIViewController ah abaixo do UIViewController (se houver, é claro)
  • presented : pesquise o UIViewController pelo fornecido (para StackIteratingFinder essa opção não faz sentido, pois sempre começa do topo)

A figura a seguir pode tornar a explicação acima mais óbvia:


Eu recomendaria se familiarizar com o conceito de contêineres em um artigo anterior.


Exemplo Se você deseja que o Finder procure um AccountViewController na pilha inteira, mas apenas entre os UIViewController visíveis, isso deve ser escrito assim:


 ClassFinder<AccountViewController, Any?>(options: [.current, .visible, .presenting]) 

NB Se, por algum motivo, as configurações fornecidas forem poucas - você sempre poderá escrever facilmente sua implementação do Finder a. Um exemplo será neste artigo.


Vamos passar, de fato, aos exemplos.


Exemplos de configurações com explicações


Eu tenho um certo UIViewController , que é o rootViewController UIWindow , e quero que ele seja substituído por um determinado HomeViewController no final da navegação:


 let screen = StepAssembly( finder: ClassFinder<HomeViewController, Any?>(), factory: XibFactory()) .using(GeneralAction.replaceRoot()) .from(GeneralStep.root()) .assemble() 

XibFactory carregará o HomeViewController partir do arquivo xib do HomeViewController.xib


Não esqueça que, se você usar implementações abstratas do Finder e Factory em conjunto, deverá especificar o tipo de UIViewController e o contexto para pelo menos uma das entidades - ClassFinder<HomeViewController, Any?>


O que acontece se, no exemplo acima, eu substituir GeneralStep.root por GeneralStep.current ?


A configuração funcionará até ser chamada no momento em que houver algum UIViewController modal na tela. Nesse caso, GeneralAction.replaceRoot não poderá substituir o controlador raiz, pois existe um controlador modal acima dele e o roteador relatará um erro. Se você deseja que essa configuração funcione de qualquer maneira, você precisa explicar ao roteador que deseja que GeneralAction.replaceRoot seja aplicado especificamente ao UIViewController raiz. Em seguida, o roteador removerá todos os UIViewController representados UIViewController e a configuração funcionará em qualquer situação.


Quero mostrar um pouco de AccountViewController , se ele ainda for exibido bem, dentro de qualquer UINavigationController e que esteja atualmente na tela em algum lugar (mesmo que esse UINavigationController sob algum UIViewController modal):


 let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(SingleStep(ClassFinder<UINavigationController, Any?>(), NilFactory())) .from(GeneralStep.current()) .assemble() 

O que o NilFactory significa nessa configuração? Por isso, você diz ao roteador que, se ele não encontrar nenhum UINavigationController na tela, não deseja que ele o crie e apenas não faça nada nesse caso. A propósito, como esse é o NilFactory , você não pode usar o Action depois dele.


Quero mostrar um pouco de AccountViewController , se ele ainda não estiver sendo mostrado, dentro de qualquer UINavigationController e que esteja atualmente em algum lugar na tela e, se isso não acontecer, crie-o e mostre-o de forma modal:


 let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.PushToNavigation()) .from(SwitchAssembly<UINavigationController, Any?>() .addCase(expecting: ClassFinder<UINavigationController, Any?>(options: .visible)) //   -    .assemble(default: { //      return ChainAssembly() .from(SingleContainerStep(finder: NilFinder(), factory: NavigationControllerFactory())) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() }) ).assemble() 

Quero mostrar UITabBarController com UITabBarController contendo HomeViewController e AccountViewController substituindo-as pela raiz atual:


 let tabScreen = SingleContainerStep( finder: ClassFinder(), factory: CompleteFactoryAssembly(factory: TabBarControllerFactory()) .with(XibFactory<HomeViewController, Any?>(), using: UITabBarController.addTab()) .with(XibFactory<AccountViewController, Any?>(), using: UITabBarController.addTab()) .assemble()) .using(GeneralAction.replaceRoot()) .from(GeneralStep.root()) .assemble() 

Posso usar o UIViewControllerTransitioningDelegate personalizado com a ação GeneralAction.presentModally :


 let transitionController = CustomViewControllerTransitioningDelegate() //     .using(GeneralAction.PresentModally(transitioningDelegate: transitionController)) 

Quero ir para o AccountViewController , onde quer que o usuário esteja, em outra guia ou mesmo em algum tipo de janela modal:


 let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: NilFactory()) .from(tabScreen) .assemble() 

Por que estamos usando o NilFactory ? Não precisamos criar um AccountViewController se ele não for encontrado. Ele será construído na configuração da tabScreen . Veja ela acima.


Eu quero mostrar ForgotPasswordViewController , mas certamente depois do LoginViewController dentro do UINavigationController :


 let loginScreen = StepAssembly( finder: ClassFinder<LoginViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(NavigationControllerStep()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() let forgotPasswordScreen = StepAssembly( finder: ClassFinder<ForgotPasswordViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(loginScreen.expectingContainer()) .assemble() 

Você pode usar a configuração no exemplo para navegação nos ForgotPasswordViewController e LoginViewController


Por que expectingContainer no exemplo acima?


Como a ação pushToNavigation exige a presença de um UINavigationController e na configuração após ele, o método expectingContainer nos permite evitar um erro de compilação, garantindo que tenhamos o cuidado de garantir que, quando o roteador atingir o loginScreen em loginScreen , o UINavigationController estará lá.


O que acontece se na configuração acima eu substituir GeneralStep.current por GeneralStep.root ?


Funcionará, mas desde que você diga ao roteador que deseja começar a construir uma cadeia a partir do UIViewController raiz, se algum UIViewController modal for aberto acima dele, o roteador os ocultará antes de começar a construir a cadeia.


Meu aplicativo possui um UITabBarController contendo HomeViewController e BagViewController como guias. Quero que o usuário possa alternar entre eles usando os ícones nas guias, como de costume. Mas se eu chamar a configuração programaticamente (por exemplo, o usuário clicar em "Ir para o Bag" dentro do HomeViewController ), o aplicativo não deve alternar a guia, mas mostrar o BagViewController .


Existem três maneiras de conseguir isso na configuração:


  1. Defina StackIteratingFinder para pesquisar apenas nos visíveis usando [.current, .visible]
  2. Use o NilFinder que significa que o roteador nunca encontrará o BagViewController nas BagViewController e sempre o criará. No entanto, essa abordagem tem um efeito colateral - se, por exemplo, um usuário já no BagViewController for apresentado de forma modal e, por exemplo, clicar em um link universal que o BagViewController deve mostrar a ele, o roteador não o encontrará e criará outra instância e mostrará acima. modalmente. Pode não ser o que você deseja.
  3. Mude um pouco o ClassFinder para encontrar apenas o BagViewController mostrado BagViewController e ignore o restante, e já o use na configuração.

 struct ModalBagFinder: StackIteratingFinder { func isTarget(_ viewController: BagViewController, with context: Any?) -> Bool { return viewController.presentingViewController != nil } } let screen = StepAssembly( finder: ModalBagFinder(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(NavigationControllerStep()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() 

Em vez de uma conclusão


Espero que os métodos de configuração do roteador tenham se tornado um pouco mais claros. Como eu disse, usamos essa abordagem em três aplicativos e ainda não encontramos uma situação em que não seja suficientemente flexível. 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, ajudando apenas a dividir o processo de composição em etapas e executando-os na sequência fornecida e testando-os nas versões 9 a 12. do iOS. , essa abordagem se encaixa em todos os padrões de arquitetura que envolvem o trabalho com a pilha UIViewController (MVC, MVVM, VIP, RIB, VIPER etc.)


Eu ficaria feliz por seus comentários e sugestões. Especialmente se você acha que vale a pena considerar alguns aspectos com mais detalhes. Talvez o conceito de contextos precise de esclarecimentos.

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


All Articles