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.
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:
ClassFinder
will not find the ProductViewController
and the router will move on.NilFinder
will never find anything and the router will move onGeneralStep.current
will always return the topmost UIViewController
in the stack.UIViewController
found, the router will turn backUINavigationController
using `NavigationControllerFactoryGeneralAction.presentModally
ProductViewController
create a ProductViewController
using the ProductViewControllerFactory
ProductViewController
into the previous UINavigationController
using UINavigationController.pushToNavigation
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:
current
: The topmost view controller in the stack. (The one that is the rootViewController
the UIWindow
or the one that is shown modally at the very top)visible
: In the event that the UIViewController
is a container — look for its visible UIViewController
ah (For example: the UINavigationController
always has one visible UIViewController
, the UISplitController
can have one or two depending on how it is presented.)contained
: In the event that the UIViewController
is a container - look in all its nested UIViewController
ah (For example: Go through all twist UINavigationController
controllers including the visible one)presenting
: Search also in all UIViewController
ah under the UIViewController
(if there are of course)presented
: Look in UIViewController
ah above the provided one (for StackIteratingFinder
this option does not make sense, as it always starts from the top)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.
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?>
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.
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.
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()
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()
UIViewControllerTransitioningDelegate
with a GeneralAction.presentModally
action: let transitionController = CustomViewControllerTransitioningDelegate() // .using(GeneralAction.PresentModally(transitioningDelegate: transitionController))
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.
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
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
.
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.
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:
StackIteratingFinder
search only visible using [.current, .visible]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.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()
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