Coordinators Part I

What is flow in mobile applications?

流是逻辑链接的屏幕队列。我们所有的屏幕都可以按流程划分:身份验证流程、电话验证流程、预订流程、个人资料编辑流程等。

How do we usually manage screens in flow?

假设我们有一个auth流,在这个流中我们需要验证用户的电话。

我们以这样一个方案为例:

avatar

我们通常如何处理这个流程中的导航?

在最常见的情况下,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()
        }
}

控制器与所有相邻的控制器完全链接,并试图对其进行管理。看起来像苏联漫画里的格罗莫泽卡:

avatar

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

avatar

假设我们有一堆控制器,我们需要在它们之间共享数据。我们如何做到这一点?

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 = 🐍
  }
}

太好了!我们创造了一个耦合流。现在项目经理来找我们说:

avatar

使流独立的另一个常见原因是它们能够在不同的地方重用。例如,假设您有一个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。拆分后,我们可以在任何地方重复使用它们。

avatar
avatar

现在我们有两个分开的流。

让我们看看引擎盖下面。

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

avatar

What do we need for Coordinators to work?

我们可以轻松地更改要使用的类的数量,但通常的设置如下所示:

  • Router, 用于导航(路由器只是路线!在我们的案例中是被动的)。
  • Modules’ factory, 用于创建模块和注入所有依赖项。
  • Coordinators’ factory (optional), 以防我们需要切换到另一个流。
  • Storage (optional), 只有我们需要存储数据并将其注入到模块中。
avatar

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可以添加一些其他数据并将其发送到服务器。

avatar
moduleA.onNext = { [weak self] dict in
  self?.storage.dict = dict
  self?.showModuleB()
}

我们可以在存储中添加一些附加逻辑,以便将数据保存在光盘上或将数据格式化为json。

Conclusion

这对于第一部分可能已经足够了。我们学到了什么好处?

  • 控制器对其他控制器一无所知
  • 控制器可以很容易地集成到不同的流中
  • 控制器不向其他控制器发送数据
  • 容易重用
  • 简化测试