在上一篇文章中,我讨论了用于在我工作的多个应用程序中的视图控制器之间进行组合和导航的方法,因此,这产生了一个单独的RouteComposer库。 我收到了有关前一篇文章的大量令人愉快的反馈以及一些实用的建议,这促使我写了另一篇文章,对如何配置库进行了进一步解释。 在裁减下,我将尝试找出一些最常见的配置。

路由器如何解析配置
首先,请考虑路由器如何解析您编写的配置。 以上一篇文章为例:
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()
路由器将从头开始执行所有步骤,直到其中一个步骤(使用提供的Finder
)通知所需的UIViewController
已经在堆栈上。 (例如,保证在控制器堆栈中存在GeneralStep.current()
。)然后,路由器将开始沿着步骤链移动,使用提供的UIViewController
创建所需的UIViewController
并使用指定的Action
集成它们。 由于即使在编译阶段也要进行类型检查,大多数情况下,您将无法使用与提供的Fabric
不兼容的UITabBarController.addTab
(也就是说,您将无法在NavigationControllerFactory
构建的UITabBarController.addTab
控制器中使用UITabBarController.addTab
)。
如果您想象上述配置,那么如果屏幕上只有某个ProductViewController
,将执行以下步骤:
ClassFinder
将找不到ProductViewController
并且路由器将继续运行NilFinder
永远找不到任何东西,路由器将继续前进GeneralStep.current
将始终返回堆栈上最顶层的UIViewController
。- 找到启动
UIViewController
,路由器将回头 - 使用`NavigationControllerFactory构建一个
UINavigationController
- 将使用
GeneralAction.presentModally
模态显示 ProductViewControllerFactory
ProductViewController
ProductViewControllerFactory
- 使用
UINavigationController.pushToNavigation
将创建的ProductViewController
集成到以前的UINavigationController
中 - 完成导航
注意: 应该理解,实际上,如果其中没有某些UIViewController
,就不可能模态地显示UINavigationController
。 因此,路由器将以略有不同的顺序执行步骤5-8。 但是您不应该考虑它。 依次描述该配置。
编写配置时的一个好习惯是假设用户现在可以位于应用程序中的任何位置,并且突然收到一条推送消息,其中包含进入到您正在描述的屏幕的请求,并尝试回答问题-“应用程序应如何表现? “,” Finder
在我描述的配置中将如何表现? 如果考虑了所有这些问题,您将获得一个配置,可以确保无论用户身在何处,都可以向其显示所需的屏幕。 这是涉及市场和吸引(吸引)用户的团队对现代应用程序的主要要求。
StackIteratingFinder
及其选项:
您可以以您认为最可接受的任何方式实施Finder
概念。 但是,最简单的方法是遍历屏幕上视图控制器的图形。 为了简化此目标,该库提供了StackIteratingFinder
和执行此任务的各种实现。 您只需要回答问题-这就是您期望的UIViewController
。
为了影响StackIteratingFinder
的行为并告诉控制器希望在图形(堆栈)的哪个部分中查找,可以在创建它时指定SearchOptions
的组合。 他们应该详细说明:
current
:堆栈中最顶层的视图控制器。 (这是UIWindow
的rootViewController
或在最上方以模态显示的那个)visible
:如果UIViewController
是一个容器,请查看其可见的UIViewController
ah(例如: UINavigationController
始终有一个可见的UIViewController
, UISplitController
其显示方式, UISplitController
可能有一个或两个。)contained
:如果UIViewController
是一个容器,请在其所有嵌套的UIViewController
搜索(例如:遍历UINavigationController
所有视图控制器,包括可见的视图控制器)presenting
:在所有UIViewController
啊中也搜索UIViewController
(如果有的话)StackIteratingFinder
:在UIViewController
中搜索提供的选项(对于StackIteratingFinder
此选项没有意义,因为它始终从顶部开始)
下图可能使上面的解释更加明显:

我建议您在上一篇文章中熟悉容器的概念。
示例如果您希望Finder
在整个堆栈中Finder
AccountViewController
,而仅在可见的UIViewController
Finder
则应这样编写:
ClassFinder<AccountViewController, Any?>(options: [.current, .visible, .presenting])
注意 如果由于某种原因提供的设置很少,您总是可以轻松编写Finder
a的实现。 本文将提供一个示例。
实际上,让我们通过示例。
配置示例及说明
我有一个特定的UIViewController
,它是UIWindow
的rootViewController
,我希望在导航结束时将其替换为某个HomeViewController
:
let screen = StepAssembly( finder: ClassFinder<HomeViewController, Any?>(), factory: XibFactory()) .using(GeneralAction.replaceRoot()) .from(GeneralStep.root()) .assemble()
XibFactory
HomeViewController
的xib文件加载HomeViewController.xib
不要忘记,如果您结合使用Finder
和Factory
抽象实现,则必须为至少一个实体ClassFinder<HomeViewController, Any?>
指定UIViewController
的类型和上下文。
如果在上面的示例中,我将GeneralStep.root
替换为GeneralStep.root
,会发生什么情况?
该配置将一直有效,直到在屏幕上出现任何模式UIViewController
时调用它为止。 在这种情况下, GeneralAction.replaceRoot
将无法替换根控制器,因为它上面有一个模态控制器,并且路由器将报告错误。 如果您仍然希望此配置正常工作,则需要向路由器说明您希望将GeneralAction.replaceRoot
专门应用于根UIViewController
。 然后,路由器将删除所有以模态表示的UIViewController
并且该配置将在任何情况下均可用。
我想在任何UINavigationController
内显示一些AccountViewController
,如果仍然可以很好地显示,并且它当前在屏幕上的某个位置(即使该UINavigationController
在某些模式UIViewController
):
let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.pushToNavigation()) .from(SingleStep(ClassFinder<UINavigationController, Any?>(), NilFactory())) .from(GeneralStep.current()) .assemble()
NilFactory
在此配置中是什么意思? 这样,您告诉路由器,如果他在屏幕上找不到任何UINavigationController
,则不希望他创建它,并且在这种情况下什么也不做。 顺便说一下,由于这是NilFactory
,因此您不能在其后使用Action
。
我想在任何UINavigationController
内部显示一个AccountViewController
(如果尚未显示),并且当前不在屏幕上,如果不是这样,则创建它并以模态显示:
let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: XibFactory()) .using(UINavigationController.PushToNavigation()) .from(SwitchAssembly<UINavigationController, Any?>() .addCase(expecting: ClassFinder<UINavigationController, Any?>(options: .visible))
我想显示UITabBarController
,其中包含HomeViewController
和AccountViewController
UITabBarController
用当前根替换:
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
与GeneralAction.presentModally
操作一起使用:
let transitionController = CustomViewControllerTransitioningDelegate()
我想转到AccountViewController
,无论用户在哪里,都在另一个选项卡甚至某种模式的窗口中:
let screen = StepAssembly( finder: ClassFinder<AccountViewController, Any?>(), factory: NilFactory()) .from(tabScreen) .assemble()
为什么在NilFactory
使用NilFactory
? 如果找不到,则不需要构建AccountViewController
。 它将在tabScreen
配置中构建。 在上面见她。
我想模态显示ForgotPasswordViewController
,但是,当然要在UINavigationController
里面的LoginViewController
之后:
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()
您可以将示例中的配置用于ForgotPasswordViewController
和LoginViewController
导航
为什么在上面的示例中使用expectingContainer
?
由于pushToNavigation
操作要求存在UINavigationController
并在其后的配置中,所以expectingContainer
方法允许我们确保当路由器在loginScreen
到达loginScreen
, UINavigationController
在那里,从而避免了编译错误。
如果在上述配置中将GeneralStep.current
替换为GeneralStep.current
会发生什么?
它将起作用,但是由于您告诉路由器要从根UIViewController
开始构建链,如果在其上方打开了任何模式UIViewController
,则路由器将在开始构建链之前将其隐藏。
我的应用程序有一个UITabBarController
其中包含HomeViewController
和BagViewController
作为选项卡。 我希望用户能够像往常一样使用选项卡上的图标在它们之间切换。 但是,如果我以编程方式调用配置(例如,用户在HomeViewController
单击“转到袋”),则应用程序不应切换选项卡,而应BagViewController
模态显示BagViewController
。
在配置中有3种方法可以实现此目的:
- 将
StackIteratingFinder
设置为仅使用[.current,.visible]搜索可见的内容 - 使用
NilFinder
意味着路由器将永远不会在BagViewController
找到BagViewController并将始终创建它。 但是,这种方法会产生副作用-例如,如果以模态显示了已经在BagViewController
过的用户,并且例如单击了BagViewController
应该向他显示的通用链接,则路由器将找不到它,而是将创建另一个实例并将其显示在其上方模态地 这可能不是您想要的。 - 更改一个小的
ClassFinder
,使其仅找到模态显示的BagViewController
而忽略其余的内容,并已在配置中使用它。
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()
而不是结论
我希望配置路由器的方法变得更加清晰。 就像我说的那样,我们在3个应用程序中使用了这种方法,但是还没有遇到它不够灵活的情况。 该库以及提供给它的路由器的实现,在运行时不使用任何客观技巧,并且完全遵循所有Cocoa Touch概念,仅有助于将合成过程分解为若干步骤,并按给定顺序执行,并在iOS 9至12版中进行了测试。 ,这种方法适用于涉及使用UIViewController
堆栈的所有架构模式(MVC,MVVM,VIP,RIB,VIPER等)
我很高兴您的意见和建议。 特别是如果您认为值得在某些方面进行更详细的介绍。 上下文的概念也许需要澄清。