📜 ⬆️ ⬇️

Configuration examples for UIViewControllers using RouteComposer

In the previous article, I talked about the approach that 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 weighty 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 make out a few of the most frequent configurations.



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 Finder ) “informs” that the desired UIViewController already on the stack. (So, for example, GeneralStep.current() guaranteed to be present on the stack with a view of controllers) Then the router starts moving back through the chain of steps creating the required UIViewController using the provided UIViewController and integrating them using the specified UIViewController . Thanks to type checking at compile time, most often, you cannot use UITabBarController.addTab that are incompatible with the provided Fabric (that is, you cannot use UITabBarController.addTab as a controller built by NavigationControllerFactory ).


If we present the configuration described above, then if you just have a non- ProductViewController on the screen, the following steps will be performed:


  1. ClassFinder will not find the ProductViewController and the router will move on.
  2. NilFinder will never find anything and the router will move on
  3. GeneralStep.current will always return the topmost UIViewController in the stack.
  4. Starting UIViewController found, the router will turn back
  5. Build UINavigationController using `NavigationControllerFactory
  6. Will show it modally using GeneralAction.presentModally
  7. ProductViewController create a ProductViewController using the ProductViewControllerFactory
  8. Integrates the created ProductViewController into the previous UINavigationController using UINavigationController.pushToNavigation
  9. Finish the navigation

NB: It should be understood that in reality it is impossible to show the modal UINavigationController without some UIViewController inside 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 can currently 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 Finders behave in the configuration that I describe? ". 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 engaging (engaging) users.


StackIteratingFinder and its options:


You can implement the Finder concept in 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 StackIteratingFinder and various implementations that will take on this task. You will only have to answer the question - is this the UIViewController that you are expecting.


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



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 Finder look for an AccountViewController in the entire stack, but only among the visible UIViewController then this should be written like this:


 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 of Finder a. One example will be in this article.


Let us proceed, in fact, to examples.


Examples of configurations with explanations


I have a certain UIViewController , which is the rootViewController the UIWindow , and I want it to be replaced with a HomeViewController after the end of the navigation:


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

XibFactory load the HomeViewController from the xib file of the HomeViewController.xib


Do not forget that if you use the abstract implementations of Finder and Factory in combination, you must specify the UIViewController type and context in at least one of the entities - ClassFinder<HomeViewController, Any?>


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


The configuration will work until it is called at the moment when there is any modal UIViewController on the screen. In this case, GeneralAction.replaceRoot will not be able to replace the root controller, 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 GeneralAction.replaceRoot be applied specifically to the root UIViewController . Then the router will remove all UIViewController and the configuration will work in any case.


I want to show a certain AccountViewController , in case it is still well shown, inside any UINavigationController and which is currently on the screen somewhere (even if this UINavigationController under a certain modal UIViewController ):


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

What does NilFactory mean in this configuration? By doing so, you tell the router that if he could not find a single UINavigationController on the screen, you do not want him to create it and that he simply did not do anything in this case. By the way, since this is NilFactory , you cannot use Action after it.


I want to show a certain AccountViewController , in case it is not yet shown, inside any UINavigationController and which currently is somewhere on the screen, and if there is no UINavigationController , 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 UITabBarController with UITabBarController containing HomeViewController and AccountViewController replacing the current UITabBarController with it:


 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 a custom UIViewControllerTransitioningDelegate with a GeneralAction.presentModally action:


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

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


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

Why do we use NilFactory ? We do not need to build an AccountViewController in case it is not found. It will be built in a tabScreen configuration. See her above.


I want to show the modal ForgotPasswordViewController , but, of LoginViewController , after LoginViewController and inside UINavigationController but:


 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 ForgotPasswordViewController and in LoginViewController


What is the expectingContainer in the example above?


Since the pushToNavigation action requires the presence of a UINavigationController and in the configuration after it, the expectingContainer method allows us to avoid compilation errors, ensuring that we take care that when the router reaches the loginScreen in loginScreen , there will be a UINavigationController .


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


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


In my application, there is a UITabBarController containing HomeViewController and BagViewController as 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 BagViewController modally.


There are 3 ways to achieve this in the configuration:


  1. Nastrit StackIteratingFinder search only visible using [.current, .visible]
  2. Use NilFinder which will mean that the router will never find the BagViewController in BagViewController and will always create it. However, this approach has a side effect - if, say, a user is already in BagViewController in a modal view, and, for example, clicks on a universal link that should show him BagViewController , then the router will not find it and create another instance and show it over it modally This may not be what you want.
  3. Modify a bit ClassFinder so that it only BagViewController shown modally and ignores the rest, and already use it in the configuration.

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

Instead of conclusion


I hope the ways to configure the router have become somewhat clearer. As I 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 tricks with objective runtime and fully follows all Cocoa Touch concepts, 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 the UIViewController stack (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.


')

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


All Articles