在iOS应用中使用Coordinator模式的真实示例

当代的编程世界充满着潮流,谈论iOS应用程序是很重要的。 希望我没有误会说协调器是近年来最时髦的建筑模式之一。 因此,我们的团队在不久前意识到了尝试这种方法的不可抗拒的愿望。 特别是因为它获得了幸运的机会-逻辑上的重大变化和应用程序导航的全面重新开发。

该文章的第一版是用俄语编写的,并在此处发布。

问题

通常,视图控制器承担太多责任:它们直接“ UINavigationController命令”以拥有UINavigationController ,与同级视图控制器“通信”,甚至实例化它们并传递给导航堆栈,总的来说,它要做很多事情不在他们的责任范围内。

避免此类问题的一种方法是协调器。 事实证明,这还算是非常方便且非常灵活的:模式代码能够管理小模块(可能是唯一的屏幕)和整个应用程序(相对而言,直接从其开始)的导航事件。 UIApplicationDelegate )。

历史

Martin Fowler在他的《企业应用程序体系结构模式》一书中将这种模式称为应用程序控制器。 Soroush Khanlou被认为是iOS环境中的第一个推广者:它始于2015年他在NSSpain会议上的演讲。然后,他的网站上出现了一个思想片段,该思想片段具有多个扩展名(例如,这个扩展名)。

然后是许多概述(“ ios协调器”查询提供了许多质量和细节各异的结果),包括有关Ray Wenderlich的教程和Paul Hudson的“用Swift入侵”的文章(在有关“大规模”视图问题的系列出版物中)控制器)。

向前运行,最引人注意的讨论主题是UINavigationController “后退”按钮的问题。 因为点击它不会被我们的代码处理,所以我们只能有一个回调。

为什么有问题? 像任何其他对象一样,存在于内存中的协调器必须由另一个对象拥有。 通常,当通过协调器构建导航系统时,其中一些会创建其他并在其上存储强大的参考。 子项完成后,父项将控制权收回,子项协调器使用的内存必须释放。

Khanlou对问题的解决方案有自己的见解,并注意到其他一些推荐的方法。 无论如何,我们会回到原点。

第一种方法

在显示任何代码之前,我想解释一下,与真实的设计原则完全一致的代码片段和使用示例在不会妨碍良好感知的地方会得到简化和缩短。

当我们第一次开始尝试模式时,我们没有足够的时间和自由度:我们必须考虑现有的设计原则和公认的导航系统。 第一个版本基于拥有并控制UINavigationController的通用“路由器”。 它可以对UIViewController实例执行与导航有关的任何操作:“按下” /“弹出”,“显示” /“关闭”,以及使用“根视图控制器”的操作。 接口示例:

 导入UIKit 
 协议路由器{ 
func present(_模块:UIViewController,动画:Bool)
func dismissModule(动画:Bool,完成:(()-> Void)?)
func push(_模块:UIViewController,
动画:布尔,
完成:(()-> Void)?)
func popModule(动画:布尔)
func setAsRoot(_模块:UIViewController)
func popToRootModule(动画:布尔)
}

特定的实现通常使用UINavigationController实例进行初始化,并且内部没有任何特殊的东西。 它有唯一的限制:参数值不能是UINavigationController实例(它有一个明显的原因:一个UINavigationController的堆栈中不能有另一个(UIKit的限制),除了present(_:,animated:)

协调器以及任何其他对象都必须归某物所有-另一个对象拥有强烈的引用。 主(或根)协调器可以由其父级拥有,但是每个人都可以创建其他任何人。 这是因为,我们选择了为提供的已创建协调器提供管理机制的类,而不是基本的协调器接口:

 班级协调员{ 
 私人var childCoordinators = [Coordinator]() 
  func add(依赖项协调器:Coordinator){ 
// ...
}
  func remove(依赖协调器:Coordinator){ 
// ...
}
  } 

该模式的隐含优势之一是与特定UIViewController的子类用法和实例化有关的封装。 因此,我们介绍了路由器和协调器之间交互的接口:

 协议可展示{ 
func present()-> UIViewController
}

因此,任何协调器都必须继承Coordinator类并实现Presentable协议,因此Router接口采用以下形式:

 协议路由器{ 
func present(_模块:可展示,动画:Bool)
func dismissModule(动画:Bool,完成:(()-> Void)?)
func push(_模块:可展示,
动画:布尔,
完成:(()-> Void)?)
func popModule(动画:布尔)
func setAsRoot(_模块:可显示)
func popToRootModule(动画:布尔)
}

(可Presentable方法还允许在模块中使用协调器,这些模块期望与UIViewController直接交互,而无需处理此模块。)

一个简短的例子:

 最后一班FirstCoordinator:协调人,可主持{ 
  func present()-> UIViewController { 
返回UIViewController()
}
  } 
 期末课程SecondCoordinator:协调人,可主持{ 
  func present()-> UIViewController { 
返回UIViewController()
}
  } 
 让nc = UINavigationController() 
let router = RouterImpl(navigationController:nc) //路由器实现
router.setAsRoot(FirstCoordinator())
  router.push(SecondCoordinator(),animated:true,completion:nil)router.popToRootModule(动画:true) 

下一个方法

然后有一天总的导航返工和绝对的表达时间自由了! 没有什么可以阻止尝试使用珍贵的start()方法来实现导航的时间-协调器版本通过简单和简洁吸引了我们。

再次提到上述Coordinator功能显然不是多余的:

 协议协调员{ 
  func add(依赖协调器:Coordinator) 
func remove(依赖协调器:Coordinator)
func start()
  } 
  class BaseCoordinator:协调器{ 
 私人var childCoordinators = [Coordinator]() 
  func add(依赖项协调器:Coordinator){ 
// ...
}
func remove(依赖协调器:Coordinator){
// ...
}
func start(){}
  } 

不幸的是,Swift没有抽象类(因为它比经典的面向对象的方法更专注于面向协议的方法),因此start()方法可能会留空或充满fatalError(_:file:line:) (强制子类覆盖它)。 就个人而言,我更喜欢第一种选择。

同时,Swift具有向协议方法添加默认实现的强大功能,因此我们的首要考虑不是关于基类,而是诸如此类:

 推广协调员{ 
  func add(依赖项协调器:Coordinator){ 
// ...
}
func remove(依赖协调器:Coordinator){
// ...
}
  } 

但是协议扩展不能具有存储的属性,这些方法显然应该基于一个。

这样,每个特定的协调器基础都应如下所示:

 期末课程SomeCoordinator:BaseCoordinator { 
 覆盖func start(){ 
// ...
}
  } 

任何必要的依赖关系都可以通过初始化程序注入。 通常,它是UINavigationController实例。

例如,显示根视图控制器的根协调器可以接受具有空堆栈的新导航控制器。

在后台,协调员可以在处理事件(更多有关此事)的同时,将导航控制器交给内部创建的协调员。 它可以根据需要在当前导航状态下运行:推送,呈现甚至替换整个导航堆栈。

可能的改进

后来发现,并不是每个协调员都创建其他协调员,因此并非所有协调员都需要从BaseCoordinator继承(接口隔离原则和内容)。 来自附近团队的一位同事建议摆脱继承并声明外部依赖项管理器的接口:

 协议CoordinatorDependencies { 
  func add(依赖协调器:Coordinator) 
func remove(依赖协调器:Coordinator)
  } 
 最后一类DefaultCoordinatorDependencies:CoordinatorDependencies { 
 私人让依赖= [Coordinator]() 
  func add(依赖项协调器:Coordinator){ 
// ...
}
func remove(依赖协调器:Coordinator){
// ...
}
  } 
 期末班SomeCoordinator:协调员{ 
 私有let依赖项:CoordinatorDependencies 
  init(dependenciesManager:CoordinatorDependencies = DefaultCoordinatorDependencies()){ 
依赖关系= dependencyManager
}
  func start(){ 
// ...
}
  } 

用户事件处理

好的,协调员以某种方式创建并启动了一个新视图。 用户很可能正在观看屏幕,并且看到了一些可交互的可视元素集:按钮,文本字段等。它们的一部分会导致导航事件,必须由相应的协调器控制。 这是通过常规授权来实现的。

假设我们有UIViewController子类:

 最后一课SomeViewController:UIViewController {} 

以及相应的协调员:

 期末班SomeCoordinator:协调员{ 
 私有let依赖项:CoordinatorDependencies 
私有弱var navigationController:UINavigationController吗?
  init(navigationController:UINavigationController, 
dependencyManager:CoordinatorDependencies = DefaultCoordinatorDependencies()){
self.navigationController = navigationController
依赖关系= dependencyManager
}
  func start(){ 
让vc = SomeViewController()
navigationController?.pushViewController(vc,动画:true)
}
  } 

该协调器被委托处理控制器的事件:

 协议SomeViewControllerRoute:类{ 
func onSomeEvent()
}
 最后一课SomeViewController:UIViewController { 
 私有弱变量路由:SomeViewControllerRoute? 
  init(route:SomeViewControllerRoute){ 
self.route =路线
super.init(nibName:无,捆绑:无)
}
 需要初始化吗?(编码器aDecoder:NSCoder){ 
fatalError(“ init(coder :)尚未实现”)
}
  @IBAction私有函数buttonAction(){ 
路线?.onSomeEvent()
}
  } 
 期末班SomeCoordinator:协调员{ 
 私有let依赖项:CoordinatorDependencies 
私有弱var navigationController:UINavigationController吗?
  init(navigationController:UINavigationController, 
dependencyManager:CoordinatorDependencies = DefaultCoordinatorDependencies()){
self.navigationController = navigationController
依赖关系= dependencyManager
}
  func start(){ 
让vc = SomeViewController(route:self)
navigationController?.pushViewController(vc,动画:true)
}
  } 
 扩展SomeCoordinator:SomeViewControllerRoute { 
func onSomeEvent(){
// ...
}
}

后退按钮处理

保罗·哈德森(Paul Hudson)在他的《利用Swift的黑客》一书中对正在讨论的主题进行了另一篇很好的评论(可以讲一个教程)。 它包含对上述“后退”按钮问题的可能解决方案的非常简单明了的解释:协调器将自己声明为所传递的导航控制器实例的委托人,并跟踪其感兴趣的事件。

这种方法至少有一个流程:只有NSObject子类才能成为UINavigationController委托。

现在,我们有了一个协调员,可以再产生一个。 后者在调用start()之后,将UIViewController推送到UINavigationController堆栈上。 只需单击UINavigationBar后退”按钮,就需要通知父协调员孩子已经完成流程。 为此,我们引入了另一个委托工具-每个子协调器都有一个委托人,其接口由其父协调器实现:

 协议CoordinatorFlowListener:类{ 
func onFlowFinished(协调员:协调员)
}
 最后一类MainCoordinator:NSObject,Coordinator { 
 私有let依赖项:CoordinatorDependencies 
私人让navigationController:UINavigationController
  init(navigationController:UINavigationController, 
dependencyManager:CoordinatorDependencies = DefaultCoordinatorDependencies()){
self.navigationController = navigationController
依赖关系= dependencyManager
super.init()
}
  func start(){ 
让someCoordinator = SomeCoordinator(navigationController:navigationController,
flowListener:自我)
依靠.add(someCoordinator)
someCoordinator.start()
}
  } 
 扩展MainCoordinator:CoordinatorFlowListener { 
  func onFlowFinished(协调员:协调员){ 
依靠。删除(协调员)
// ...
}
  } 
 最终课程SomeCoordinator:NSObject,Coordinator { 
 私有弱变量flowListener:CoordinatorFlowListener? 
私有弱var navigationController:UINavigationController吗?
  init(navigationController:UINavigationController, 
flowListener:CoordinatorFlowListener){
self.navigationController = navigationController
self.flowListener = flowListener
super.init()
}
  func start(){ 
// ...
}
  } 
 扩展SomeCoordinator:UINavigationControllerDelegate { 
  func navigationController(_ navigationController:UINavigationController, 
didShow viewController:UIViewController,
动画:布尔){
警卫让fromVC = navigationController.transitionCoordinator?.viewController(forKey:.from)else {
返回
}
如果navigationController.viewControllers.contains(fromVC){
返回
}
 如果fromVC是SomeViewController { 
flowListener?.onFlowFinished(协调员:自我)
}
}
  } 

在上面的示例中, MainCoordinator不执行任何操作:仅启动另一个协调器的流程-在现实世界中,这当然是没有用的。 在谈论我们的应用程序时, MainCoordinator从外部和必须显示的屏幕中获取确定当前应用程序状态(授权,未授权等)的信息。 据此,协调员将启动适当的儿童协调员。 当后者完成其流程时,主协调器立即通过CoordinatorFlowListener知道这一点,并启动另一个协调器的流程。

结论

陷入束缚的解决方案显然有很多缺点(以及对任何问题的任何解决方案)。

是的,有很多委派,但是这很简单,并且只有一个方向:从子级到父级(从视图控制器到协调器,从子级协调器到其父级)。

是的,要避免内存泄漏,必须由协调器以几乎相同的方式实现UINavigationController委托。 (第一种方法没有这个缺陷,但是更慷慨地分享其内部对特定协调者目的的了解,以作为回报。)

最大的缺点是,不幸的是,在现实世界中,协调员对外向事物的了解比所期望的要多一点。 更具体地说,协调器必须由逻辑元素填充,这些逻辑元素取决于外部环境,并且不能直接由协调器知道-发生在start()调用或onFlowFinished(coordinator:)回调中的所有内容。 可以有任何东西,而且总是会采用硬编码的行为:将视图控制器添加到导航堆栈,替换堆栈,返回到根视图控制器等。–任何操作都不取决于协调者的职责,而是取决于外部条件。

但是,代码变得非常漂亮,简洁,易于使用,并且跟踪导航路线变得更加容易。 在我们看来,意识到所提到的缺陷,解决这些缺陷的可能性更大。

感谢您来到这里,希望您已经学到了一些有用的信息! 如果您想要“更多我”,欢迎来到我的Twitter!