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:
- ClassFindernão encontrará o- ProductViewControllere o roteador continuará
- NilFindernunca encontrará nada e o roteador seguirá em frente
- GeneralStep.currentsempre retornará o- UIViewControllermais alto da pilha.
- Se o UIViewControllerencontrado, o roteador retornará
- Cria um UINavigationControllerusando o `NavigationControllerFactory
- Vai mostrá-lo modalmente usando GeneralAction.presentModally
- ProductViewControllerProductViewController- ProductViewControllerFactory
- Integra o ProductViewControllercriado aoUINavigationControlleranterior usandoUINavigationController.pushToNavigation
- 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- UIWindowou o que é mostrado modalmente no topo)
- visible: se o- UIViewControllerfor um contêiner, procure em seu- UIViewControllervisível (por exemplo:- UINavigationControllersempre possui um- UIViewControllervisível, o- UISplitControllerpode ter um ou dois, dependendo de como é apresentado.)
- contained: No caso de o- UIViewControllerser um contêiner, procure em todos os seus- UIViewControlleraninhados (por exemplo: Percorra todos os controladores de exibição do- UINavigationControllerincluindo o visível)
- presenting: pesquise também em todos os- UIViewControllerah abaixo do- UIViewController(se houver, é claro)
- presented: pesquise o- UIViewControllerpelo fornecido (para- StackIteratingFinderessa 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))  
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()  
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:
- Defina StackIteratingFinderpara pesquisar apenas nos visíveis usando [.current, .visible]
- Use o NilFinderque significa que o roteador nunca encontrará o BagViewController nasBagViewControllere sempre o criará. No entanto, essa abordagem tem um efeito colateral - se, por exemplo, um usuário já noBagViewControllerfor apresentado de forma modal e, por exemplo, clicar em um link universal que oBagViewControllerdeve 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.
- Mude um pouco o ClassFinderpara encontrar apenas oBagViewControllermostradoBagViewControllere 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.