Configuration examples of UIViewControllers using RouteComposer

    In the previous article, I talked about the approach we use to implement composition and navigation between view controllers in several applications that I work on, which ultimately resulted in a separate library RouteComposer . I received a significant amount of pleasant responses to the previous article and some practical advice that prompted me to write one more, which would explain a little more how the library was configured. Under the cut, I will try to disassemble some of the most frequent configuration.



    How the router parses the configuration


    To begin, consider how the router parses the configuration you wrote. Take the example from the previous article:


    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()

    The router will go through the chain of steps starting from the very first, until one of the steps (using the provided one Finder) “informs” that the desired one is UIViewControlleralready on the stack. (For example, it is GeneralStep.current()guaranteed to be present in the stack of view controllers) Then the router will start moving back along the chain of steps creating the required ones UIViewControllerusing the provided ones Fabricand integrating them using the specified ones Action. Thanks to type checking at compile time, more often than not, you will not be able to use Actions incompatible with the provided one Fabric(that is, you will not be able to use UITabBarController.addTaba view controller built NavigationControllerFactory).


    If you imagine the configuration described above, then if you just have ProductViewControllersomething not on the screen , then the following steps will be performed:


    1. ClassFinderwill not find ProductViewControllerand the router will move on
    2. NilFinder will never find anything and the router will move on
    3. GeneralStep.currentwill always return the topmost one UIViewControllerin the stack.
    4. Starting UIViewControllerfound, the router will turn back
    5. Build UINavigationControllerusing `NavigationControllerFactory
    6. Will show it modally using GeneralAction.presentModally
    7. ProductViewControllerWill create usingProductViewControllerFactory
    8. Integrates created ProductViewControllerin the previous UINavigationControlleripolzuyaUINavigationController.pushToNavigation
    9. Finish the navigation

    NB: It should be understood that in reality it cannot be shown modally UINavigationControllerwithout some kind UIViewControllerinside it. Therefore, steps 5-8 will be executed by the router in a slightly different order. But this should not be thought of. The configuration is described sequentially.


    A good practice when writing a configuration is the assumption that the user at the moment can be anywhere in your application, and, suddenly, receives a push message demanding to get to the screen you are describing and try to answer the question - "How should an application behave? ? "," How will you behave Finderin the configuration that I am describing? ". If all these questions are taken into account - you get a configuration that is guaranteed to show the user the desired screen wherever he is. And this is the main requirement for modern applications from the teams involved in marketing and attracting (angled) users.


    StackIteratingFinder and its options:


    You can implement the concept Finderin any way that you consider most appropriate. However, the easiest is to iterate over the graph of view controllers on the screen. To simplify this goal, the library provides StackIteratingFindervarious implementations that will take on this task. You just have to answer the question - is this the one UIViewControllerthat you expect.


    In order to influence the behavior StackIteratingFinderand tell it in which parts of the graph (stack) I twist the controllers you want it to look for, you can specify a combination when creating it SearchOptions. And they should be discussed in more detail:


    • current: The highest view controller in the stack. (The one that is rootViewControllerat UIWindowor the one that is shown modally at the very top)
    • visible: In the event that it UIViewControlleris a container, look for its visible UIViewControllerah (For example: you UINavigationControlleralways have one visible UIViewController, UISplitControllerthey can have one or two depending on how it is presented.)
    • contained: In the event that it UIViewControlleris a container - look in all its nested ahs UIViewController(For example: Go through all view controllers UINavigationControllerincluding the visible one)
    • presenting: Search also in all UIViewControllerah under the topmost (if there are of course)
    • presented: Search in UIViewControllerah over the provided one (for StackIteratingFinderthis option it does not make sense, since it always starts from the topmost one)

    The following figure may make the explanation above more visual:


    I would recommend to familiarize with the concept of containers in the previous article.


    Example If you want your Findersearch AccountViewControllerin the whole stack, but only among the visible ones, UIViewControllerthen this should be written as:


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

    NB If for some reason the settings provided are few, you can always easily write your own implementation Finder. One example will be in this article.


    Let's move on to examples.


    Examples of configurations with explanations


    I have UIViewControllerone that is rootViewControllerom UIWindow, and I want it to be replaced with a certain one after the end of navigation HomeViewController:


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

    XibFactorywill load HomeViewControllerfrom xib fileHomeViewController.xib


    Remember that if you use abstract implementations Finderand Factoryin combination, you must specify the type UIViewControllerand context of at least one of the entitiesClassFinder<HomeViewController, Any?>


    What happens if, in the example above, I replace GeneralStep.rootwith GeneralStep.current?


    The configuration will work until it is called at the moment when there is any modal on the screen UIViewController. In this case, the GeneralAction.replaceRootroot controller cannot be replaced, since there is a modal controller above it, and the router will report an error. If you want this configuration to work in any case, then you need to explain to the router that you want it to GeneralAction.replaceRootbe applied to the root one UIViewController. Then the router will remove all modally presented UIViewControllers and the configuration will work in any case.


    I want to show some AccountViewController, if it is still well shown, inside of anyone, UINavigationControllerand which is currently somewhere on the screen (even if this one is UINavigationControllerunder a certain modal UIViewControllerom):


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

    What does this configuration mean NilFactory? By doing so, you tell the router that if he couldn’t find one UINavigationControlleron the screen, you don’t want him to create it and that he just didn’t do anything in this case. By the way, since this NilFactoryis - you can not use Actionafter it.


    I want to show a certain one AccountViewController, in case it is not yet shown, inside anyone UINavigationControllerand who currently is somewhere on the screen, and if there is UINavigationControllerno one, create it and show it modally:


    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()

    I want to show UITabBarControllerwith tabs containing HomeViewControllerand AccountViewControllerreplacing them with the current root:


    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()

    Can I use custom UIViewControllerTransitioningDelegatewith action GeneralAction.presentModally:


    let transitionController = CustomViewControllerTransitioningDelegate()
    // Где нужно в конфигурации
    .using(GeneralAction.PresentModally(transitioningDelegate: transitionController))

    I want to go to AccountViewController, wherever the user is, in another tab or even in a modal window:


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

    Why are we using here NilFactory? We do not need to build AccountViewControllerin case it is not found. It will be built in configuration tabScreen. See her above.


    I want to show modally ForgotPasswordViewController, but, of course, after LoginViewControllera inside UINavigationControllera:


    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()

    You can use the configuration in the example for navigation and in ForgotPasswordViewControllerand inLoginViewController


    Why expectingContainerin the example above?


    Since the action pushToNavigationrequires the presence of UINavigationControllera in the configuration after it, the method expectingContainerallows us to avoid compilation errors, ensuring that we take care that when the router reaches the loginScreenruntime, UINavigationControllerit will be there.


    What happens if in the configuration above I replace GeneralStep.currentwith GeneralStep.root?


    It will work, but since you tell the router that you want it to start building a chain from the root button UIViewController, then if any modal UIViewControllers are opened over it , the router will hide them before starting to build the chain.


    In my application there is a UITabBarControllercontaining HomeViewControllerand BagViewControlleras tabs. I want the user to switch between them using the icons on the tabs as usual. But if I call the configuration programmatically (for example, the user clicks "Go to Bag" inside HomeViewController), the application should not switch the tab, but show BagViewControllermodally.


    Here are 3 ways to achieve this in the configuration:


    1. Nastray StackIteratingFindersearch only visible using [.current, .visible]
    2. To use NilFinderwhat will mean that the router will never find the one available in tabs BagViewControllerand will always create it. However, this approach has a side effect - if, say, a user is already modalized in an BagViewControllere, and, let's say, clicks on a universal link that should show him BagViewController, then the router will not find it and create another instance and show it modally. This may not be what you want.
    3. Modify a bit ClassFinderso that it finds only BagViewControllerthe modal shown and ignores the rest, and already use it in the configuration.

    structModalBagFinder: StackIteratingFinder{
        funcisTarget(_ 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()

    Instead of conclusion


    I hope the ways to configure the router have become somewhat clearer. As I already said, we use this approach in 3 applications and have not yet encountered a situation where it would not be flexible enough. The library, as well as the implementation of the router provided to it, does not use any objective-run tricks and completely follows all the concepts of Cocoa Touch, only helping to break the composition process into steps and performs them in a given sequence and tested with iOS versions 9 to 12. In addition This approach fits into all architectural patterns that imply working with a UIViewControllerstack (MVC, MVVM, VIP, RIB, VIPER, etc.)


    I will be glad to your comments and suggestions. Especially if you think that some aspects should be discussed in more detail. Perhaps the concept of contexts requires clarification.


    Also popular now: