Coordinators Part I
What is flow in mobile applications?
流是逻辑链接的屏幕队列。我们所有的屏幕都可以按流程划分:身份验证流程、电话验证流程、预订流程、个人资料编辑流程等。
How do we usually manage screens in flow?
假设我们有一个auth流,在这个流中我们需要验证用户的电话。
我们以这样一个方案为例:

我们通常如何处理这个流程中的导航?
在最常见的情况下,controller告诉router推送一个新的控制器。在代码中是这样的:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if 🍌🍌 {
router.openControllerA()
} else if 🍌🍌🍌 {
router.openControllerB()
} else if 🍐 {
router.openControllerC()
} else if 🍐🍐 {
router.openControllerD()
}
}
控制器与所有相邻的控制器完全链接,并试图对其进行管理。看起来像苏联漫画里的格罗莫泽卡:

下一种常见情况是在控制器之间发送数据。

假设我们有一堆控制器,我们需要在它们之间共享数据。我们如何做到这一点?
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "openControllerA" {
let vc = segue.destinationViewController as! ControllerA
vc.monkey = 🐒
} else if segue.identifier == "openControllerB" {
let vc = segue.destinationViewController as! ControllerB
vc.tiger = 🐯
} else if segue.identifier == "openControllerC" {
let vc = segue.destinationViewController as! ControllerC
vc.frog = 🐸
} else if segue.identifier == "openControllerD" {
let vc = segue.destinationViewController as! ControllerD
vc.snake = 🐍
}
}
太好了!我们创造了一个耦合流。现在项目经理来找我们说:

使流独立的另一个常见原因是它们能够在不同的地方重用。例如,假设您有一个phone verify流,并成功地将其用作auth流的一部分。现在,如果您需要在配置文件中使用相同的流,you can。
Summary of the introduction
直接处理流的方法通常结果很糟糕:
- controllers are hard to reuse
- every controller knows about other controllers
- hard to change flow
- hard to test
Coordinators
Module
首先,让我们介绍一下模块。模块是MVC、MVP、MVVM和Viper架构方法的一个架构元素。在最简单的情况下,对于MVC,module是一个控制器,对于Viper,module是一个Interactor-Presenter视图。
我们需要做的是编排模块。
What is Coordinator?
Coordinator是一个处理导航流的对象,它在切换到下一个链之后为下一个Coordinator共享流的处理。这意味着协调器应该只在屏幕之间保持导航逻辑。协调器可以保留对存储的引用,存储包含工厂用于创建模块的数据,但它们永远不能处理模块的业务逻辑,也永远不能驱动单元的行为。只有在点击一个单元格后按下下一个屏幕时,才会发生单元格交互。
Why should we use it?
主要目标是不受模块间职责限制,并完全独立地构建模块。在这个迭代之后,我们可以很容易地在不同的流中重用它们。
回到前面的示例,让我们看看如何改进auth流。首先,我们需要分离两个不同的流:auth和phone verify。拆分后,我们可以在任何地方重复使用它们。


现在我们有两个分开的流。
让我们看看引擎盖下面。
What parts does the Coordinator consist of?
让我们从通用接口开始。
所有Coordinator必须遵守协议:
protocol Coordinator: class {
func start()
}
这意味着如果您想运行一个新的流,您需要使用factory创建一个协调器并调用start()。
所有协调器都有单独的finish块来支持业务规则:
protocol CoordinatorOutput {
var finishFlow: (Item -> Void)? { get set }
}
例如,如果流的结果应该是创建item,那么协调器可以将其返回到finish块。在这种情况下,协调器的接口将由两个协议组成,Coordinator & CoordinatorOutput.
在身份验证流期间,用户必须验证用户的电话。这是两个独立的流(auth和phone verify),但是当我们运行auth Coordinator时,我们并不关心这个逻辑。我们只需创建协调器,配置finish块,然后调用start()。

What do we need for Coordinators to work?
我们可以轻松地更改要使用的类的数量,但通常的设置如下所示:
- Router, 用于导航(路由器只是路线!在我们的案例中是被动的)。
- Modules’ factory, 用于创建模块和注入所有依赖项。
- Coordinators’ factory (optional), 以防我们需要切换到另一个流。
- Storage (optional), 只有我们需要存储数据并将其注入到模块中。

Coordinator’s initialization:
final class ItemCoordinator {
private let factory: ItemModuleFactory
private let coordinatorFactory: CoordinatorFactory
private let router: Router
init(router: Router,
factory: ItemModuleFactory,
coordinatorFactory: CoordinatorFactory) {
self.router = router
self.factory = factory
self.coordinatorFactory = coordinatorFactory
}
}
协调器的主体由两类函数组成。如果您需要启动一个新模块,第一个func应该以“show”前缀命名:
func showItemList() {
let itemsView = factory.makeItemsView()
itemsView.onItemSelect = { [weak self] (item) in
self?.showItemDetail(item)
}
itemsView.onCreateButtonTap = { [weak self] in
self?.runCreationCoordinator()
}
router.setRootModule(itemsView)
}
我们在第一个函数中做什么?我们要求工厂创建一个模块,配置回调,并要求路由器push它。
第二个func运行新的流,因为我们需要在它们之间切换。其名称始终以“run”前缀开头:
func runCreationFlow() {
let (coordinator, module) = coordinatorFactory.makeItemCoordinatorBox()
coordinator.finishFlow = { [weak self, weak coordinator] item in
self?.router.dismissModule()
self?.removeDependency(coordinator)
self?.showItemDetail(item)
}
addDependency(coordinator)
router.present(module)
coordinator.start()
}
工厂创建第一个流的模块和协调器。我们推送模块,协调器运行流。因为协调器有一个finish块,所以我们必须配置删除所有依赖项、解除模块并显示流处理的结果所创建的项。
所有协调器都从基本协调器继承。基本协调器类保留所有依赖逻辑。出于内存管理的原因,我们需要它来保持对所有子协调器的强引用,并允许协调器调用其子函数(这就是深度链接的工作原理)。
基本协调器类如下所示:
class BaseCoordinator {
var childCoordinators: [Coordinator] = []
// add only unique object
func addDependency(_ coordinator: Coordinator) {
for element in childCoordinators {
if element === coordinator { return }
}
childCoordinators.append(coordinator)
}
func removeDependency(_ coordinator: Coordinator?) {
guard
childCoordinators.isEmpty == false,
let coordinator = coordinator
else { return }
for (index, element) in childCoordinators.enumerated() {
if element === coordinator {
childCoordinators.remove(at: index)
break
}
}
}
}
从这个例子可以理解,父协调器拥有所有子对象的引用。
Router
我们来谈谈路由器。我们为什么要使用这种模式?为什么我们不能保持UINavigationController的引用和协调器本身的推送?
首先,它打破了单一责任原则: coordinator管理流,但从不负责routing。
其次,我们可以支持iPad,也可以添加自定义屏幕过渡。Coordinator永远不会知道,因为它可以继续使用路由器的协议接口,比如“push”,而不关心新的转换动画。推送的实现细节由router决定。因此,我们可以为不同的设备(如iPhone、iPad、TV或Watch)创建路由器类的组合,并使它们符合一个通用协议。
路由器的funcs 用 Presentable protocol作为参数:
func push(_ module: Presentable?)
Presentable looks like this:
protocol Presentable {
func toPresent() -> UIViewController?
}
它是为灵活性和抽象性而设计的。我们可以根据需要创建一个复杂的模块,但是需要提取UIViewController以供路由器使用。模块将使用哪种方法获取此控制器并不重要。
Factory
我相信每个人都知道我们应该使用这种模式的情况。在我们的例子中,它的工作原理类似于Viper的组装,这意味着工厂负责模块的构建。它创建所有对象,设置所有依赖项,注入属性,并返回协议。
在一个简单的情况下,它将只是一个控制器:
func makeItemDetailOutput(item: Item) -> ItemDetailView {
let controller = ItemDetailController.controllerFromStoryboard(.items)
controller.item = item
return controller
}
我们返回协议是因为它易于测试,在这种情况下,我们遵循开-关原则。
在我们的项目中,我们为模块和协调器使用了两个工厂类。在我们的团队中,我们经历了很多圣战,决定在哪些情况下我们应该建立一个工厂。为每个流创建一个工厂类,还是将所有流都保存在一个工厂类中?问题是,如果我们在不同的流中重用一个模块,我们必须在不同的工厂中复制粘贴这些方法。所以我们决定将所有模块放在一个工厂类中,将所有协调器放在另一个工厂类中。对于每一个流程,我们都会创建包含所有必要模块的工厂协议,如下所示:
protocol ItemModuleFactory {
func makeItemsOutput() -> ItemsListView
func makeItemDetailOutput(item: ItemList) -> ItemDetailView
}
工厂的类应符合所有这些协议:
class ModuleFactoryImp:
AuthModuleFactory,
ItemModuleFactory,
ItemCreateModuleFactory,
SettingsModuleFactory {
/* implementation */
}
或者coordinator工厂我们只创建一个协议,但每个协调员都应该有可能创建任何其他coordinator。
Storage
存储器保存工厂用于创建下一个模块的所有数据。
例如,我们想要创建一个对象,我们的流由三个模块组成:A、B和C。在一些工作之后,我们从模块a检索字典,从模块B检索字符串。最后,我们将这些数据发送到模块C。然后,模块C可以添加一些其他数据并将其发送到服务器。

moduleA.onNext = { [weak self] dict in
self?.storage.dict = dict
self?.showModuleB()
}
我们可以在存储中添加一些附加逻辑,以便将数据保存在光盘上或将数据格式化为json。
Conclusion
这对于第一部分可能已经足够了。我们学到了什么好处?
- 控制器对其他控制器一无所知
- 控制器可以很容易地集成到不同的流中
- 控制器不向其他控制器发送数据
- 容易重用
- 简化测试