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:
ClassFinder
não encontrará o ProductViewController
e o roteador continuaráNilFinder
nunca encontrará nada e o roteador seguirá em frenteGeneralStep.current
sempre retornará o UIViewController
mais alto da pilha.- Se o
UIViewController
encontrado, o roteador retornará - Cria um
UINavigationController
usando o `NavigationControllerFactory - Vai mostrá-lo modalmente usando
GeneralAction.presentModally
ProductViewController
ProductViewController ProductViewControllerFactory
- Integra o
ProductViewController
criado ao UINavigationController
anterior usando UINavigationController.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
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))
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
StackIteratingFinder
para pesquisar apenas nos visíveis usando [.current, .visible] - 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. - 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.