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 UIViewController
already 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 UIViewController
using the provided ones Fabric
and 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 Action
s incompatible with the provided one Fabric
(that is, you will not be able to use UITabBarController.addTab
a view controller built NavigationControllerFactory
).
If you imagine the configuration described above, then if you just have ProductViewController
something not on the screen , then the following steps will be performed:
ClassFinder
will not findProductViewController
and the router will move onNilFinder
will never find anything and the router will move onGeneralStep.current
will always return the topmost oneUIViewController
in the stack.- Starting
UIViewController
found, the router will turn back - Build
UINavigationController
using `NavigationControllerFactory - Will show it modally using
GeneralAction.presentModally
ProductViewController
Will create usingProductViewControllerFactory
- Integrates created
ProductViewController
in the previousUINavigationController
ipolzuyaUINavigationController.pushToNavigation
- Finish the navigation
NB: It should be understood that in reality it cannot be shown modally UINavigationController
without some kind 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 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 Finder
in 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 Finder
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
various implementations that will take on this task. You just have to answer the question - is this the one UIViewController
that you expect.
In order to influence the behavior StackIteratingFinder
and 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 isrootViewController
atUIWindow
or the one that is shown modally at the very top)visible
: In the event that itUIViewController
is a container, look for its visibleUIViewController
ah (For example: youUINavigationController
always have one visibleUIViewController
,UISplitController
they can have one or two depending on how it is presented.)contained
: In the event that itUIViewController
is a container - look in all its nested ahsUIViewController
(For example: Go through all view controllersUINavigationController
including the visible one)presenting
: Search also in allUIViewController
ah under the topmost (if there are of course)presented
: Search inUIViewController
ah over the provided one (forStackIteratingFinder
this 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 Finder
search AccountViewController
in the whole stack, but only among the visible ones, UIViewController
then 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 UIViewController
one that is rootViewController
om 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()
XibFactory
will load HomeViewController
from xib fileHomeViewController.xib
Remember that if you use abstract implementations Finder
and Factory
in combination, you must specify the type UIViewController
and context of at least one of the entitiesClassFinder<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 on the screen UIViewController
. In this case, the GeneralAction.replaceRoot
root 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.replaceRoot
be applied to the root one UIViewController
. Then the router will remove all modally presented UIViewController
s and the configuration will work in any case.
I want to show some AccountViewController
, if it is still well shown, inside of anyone, UINavigationController
and which is currently somewhere on the screen (even if this one is UINavigationController
under a certain modal UIViewController
om):
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 UINavigationController
on 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 NilFactory
is - you can not use Action
after it.
I want to show a certain one AccountViewController
, in case it is not yet shown, inside anyone UINavigationController
and who currently is somewhere on the screen, and if there is UINavigationController
no 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 UITabBarController
with tabs containing HomeViewController
and AccountViewController
replacing 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 UIViewControllerTransitioningDelegate
with 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 AccountViewController
in 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 LoginViewController
a inside UINavigationController
a:
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 inLoginViewController
Why expectingContainer
in the example above?
Since the action pushToNavigation
requires the presence of UINavigationController
a in the configuration after it, the method expectingContainer
allows us to avoid compilation errors, ensuring that we take care that when the router reaches the loginScreen
runtime, UINavigationController
it will be there.
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 button UIViewController
, then if any modal UIViewController
s 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.
Here are 3 ways to achieve this in the configuration:
- Nastray
StackIteratingFinder
search only visible using [.current, .visible] - To use
NilFinder
what will mean that the router will never find the one available in tabsBagViewController
and will always create it. However, this approach has a side effect - if, say, a user is already modalized in anBagViewController
e, and, let's say, clicks on a universal link that should show himBagViewController
, then the router will not find it and create another instance and show it modally. This may not be what you want. - Modify a bit
ClassFinder
so that it finds onlyBagViewController
the 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 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.