在真实的“ iOS”项目中使用“协调器”的经验

现代编程的世界充满趋势,而对于“ iOS”应用程序的编程世界来说,这是双倍的事实。 我希望在断言近年来最“时尚”的建筑模式之一是“协调员”方面没有大错。 因此,我们的团队不久前意识到了对自己尝试这项技术的不可抗拒的愿望。 此外,出现了一个非常好的案例-应用程序中的导航逻辑和总体重新计划有了重大改变。

问题


通常,控制器开始承担过多的任务:直接向UINavigationController发出“命令”,与他们的“兄弟”控制器“通信”(甚至将它们初始化并将它们传递给导航堆栈)-通常,要做很多事情他们甚至都不应该怀疑。

避免这种情况的一种可能方法就是“协调器”。 而且,事实证明,它非常方便工作且非常灵活:该模板能够管理两个小模块(可能仅代表一个屏幕)和整个应用程序(相对而言,直接从其启动“流程”)的导航事件。 UIApplicationDelegate )。

故事


马丁·福勒(Martin Fowler)在其《 企业应用程序体系结构的模式》一书中将这种模式称为Application ControllerSorush Khanlu是他在“ iOS”环境中的第一个推广 :这一切始于在2015 年发布的“ NSSpain”报告 。 然后,一篇评论文章出现在他的网站上 ,该文章有几个续集(例如this )。

然后进行了很多评论(“ ios协调器”查询给出了许多质量和细节程度不同的结果),甚至包括有关Ray Wenderlich指南以及Paul Hudson的文章“ Hacking with Swift” ,这是有关如何解决问题的一系列材料的一部分“大型”控制器。

展望未来,讨论中最值得注意的主题是UINavigationController中的后退按钮的问题,我们的代码未处理该按钮的单击,但只能得到回调

其实为什么这是一个问题? 像任何对象一样,协调器为了存在于内存中,还需要其他一些对象来“拥有”它们。 通常,在使用协调器构建导航系统时,一些协调器会生成其他协调器,并与它们保持牢固的链接。 当“离开原始协调者的责任区”时,控制权返回到原始协调者,并且必须释放原始发起者所占用的内存。

索鲁什(Sorush)有解决这个问题的自己的见解 ,并指出了一些有价值的 方法 。 但是,我们将回到这一点。

第一种方法


在开始展示真实代码之前,我想澄清一下,尽管这些原则与我们在项目中提出的那些原则完全一致,但只要不干扰代码的理解,就会简化并减少代码摘录及其使用示例。

当我们刚开始与团队中的协调员进行实验时,我们没有太多的时间和行动自由:必须考虑现有的原理和导航装置。 协调器的第一个实现选项是基于公用的“路由器”,该路由器由UINavigationController拥有和操作。 他知道如何处理UIViewController实例,这涉及导航-推/弹出,呈现/关闭以及根控制器的操作 。 这样的路由器的接口示例:

 import UIKit protocol Router { func present(_ module: UIViewController, animated: Bool) func dismissModule(animated: Bool, completion: (() -> Void)?) func push(_ module: UIViewController, animated: Bool, completion: (() -> Void)?) func popModule(animated: Bool) func setAsRoot(_ module: UIViewController) func popToRootModule(animated: Bool) } 

一个特定的实现是使用UINavigationController的实例进行初始化的,其本身并不包含任何棘手的问题。 唯一的限制:您不能将UINavigationController其他实例作为参数传递给接口方法(出于明显的原因: UINavigationController不能在其堆栈中包含UINavigationController这是UIKit限制)。

像任何对象一样,协调器也需要一个所有者-另一个将存储指向该对象的链接的对象。 到根的链接可以由生成根的对象存储,但是每个协调器也可以生成其他协调器。 因此,编写了一个基本接口来为生成的协调器提供管理机制:

 class Coordinator { private var childCoordinators = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } 

协调器的隐式好处之一是封装了有关UIViewController特定子类的知识。 为了确保路由器和协调器之间的交互,我们引入了以下接口:

 protocol Presentable { func presented() -> UIViewController } 

然后,每个特定的协调器都应从Coordinator继承并实现Presentable接口,并且路由器接口应采用以下形式:

 protocol Router { func present(_ module: Presentable, animated: Bool) func dismissModule(animated: Bool, completion: (() -> Void)?) func push(_ module: Presentable, animated: Bool, completion: (() -> Void)?) func popModule(animated: Bool) func setAsRoot(_ module: Presentable) func popToRootModule(animated: Bool) } 

(使用Presentable的方法还允许您在编写的模块内部使用协调器,以直接与UIViewController实例进行交互,而无需对它们(模块)进行根本处理。)

这一切的简要示例:

 final class FirstCoordinator: Coordinator, Presentable { func presented() -> UIViewController { return UIViewController() } } final class SecondCoordinator: Coordinator, Presentable { func presented() -> UIViewController { return UIViewController() } } let nc = UINavigationController() let router = RouterImpl(navigationController: nc) // Router implementation. router.setAsRoot(FirstCoordinator()) router.push(SecondCoordinator(), animated: true, completion: nil) router.popToRootModule(animated: true) 

下一个近似


然后有一天,是时候彻底改变了导航方式,实现了绝对的表达自由! 没有任何事情阻止我们尝试使用梦start()以求的start()方法在协调器上实现导航的那一刻-该版本最初以其简洁和简洁而着迷。

上面提到的Coordinator功能显然不是多余的。 但是需要将相同的方法添加到常规接口:

 protocol Coordinator { func add(dependency coordinator: Coordinator) func remove(dependency coordinator: Coordinator) func start() } class BaseCoordinator: Coordinator { private var childCoordinators = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } func start() { } } 

“ Swift”不提供声明抽象类的能力(因为它比面向经典的,面向对象的方法更面向一种面向协议的方法),因此start()方法可以留有一个空的实现或推力。那里有类似fatalError(_:file:line:) (强制用继承人覆盖此方法)。 就个人而言,我更喜欢第一种选择。

但是Swift有很大的机会将默认的实现方法添加到协议方法中,因此,首先想到的当然不是声明​​基类,而是这样做:

 extension Coordinator { func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } 

但是协议扩展不能声明存储的字段,并且这两种方法的实现显然应该基于私有的存储类型属性。

任何特定协调员的基础将如下所示:

 final class SomeCoordinator: BaseCoordinator { override func start() { // ... } } 

协调程序运行所需的任何依赖关系都可以添加到初始化程序中。 典型的情况是UINavigationController的实例。

如果这是负责协调根UIViewController的根协调器,则该协调器可以例如接受具有空堆栈的UINavigationController的新实例。

当处理事件时(稍后会详细介绍),协调器可以将此UINavigationController进一步传递给它生成的其他协调器。 他们还可以使用当前导航状态来完成所需的工作:“按下”,“呈现”,并至少替换整个导航堆栈。

可能的界面改进


后来发现,并非每个协调器都会生成其他协调器,因此并非所有协调器都应该依赖于此类基类。 因此,相关团队的一位同事建议摆脱继承并引入依赖项管理器接口作为外部依赖项:

 protocol CoordinatorDependencies { func add(dependency coordinator: Coordinator) func remove(dependency coordinator: Coordinator) } final class DefaultCoordinatorDependencies: CoordinatorDependencies { private let dependencies = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies init(dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { dependencies = dependenciesManager } func start() { // ... } } 

处理用户生成的事件


好了,协调员创建了某种方式并启动了新的映射。 用户很可能看屏幕,看到可以与之交互的一组视觉元素:按钮,文本字段等。其中一些会引发导航事件,并且它们必须由生成此控制器的协调器控制。 为了解决这个问题,我们使用传统的委托

假设有一个UIViewController的子类:

 final class SomeViewController: UIViewController { } 

和将其添加到堆栈中的协调器:

 final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager } func start() { let vc = SomeViewController() navigationController?.pushViewController(vc, animated: true) } } 

我们将相应控制器事件的​​处理委托给同一协调器。 实际上,这里使用了经典方案:

 protocol SomeViewControllerRoute: class { func onSomeEvent() } final class SomeViewController: UIViewController { private weak var route: SomeViewControllerRoute? init(route: SomeViewControllerRoute) { self.route = route super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @IBAction private func buttonAction() { route?.onSomeEvent() } } final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager } func start() { let vc = SomeViewController(route: self) navigationController?.pushViewController(vc, animated: true) } } extension SomeCoordinator: SomeViewControllerRoute { func onSomeEvent() { // ... } } 

处理返回按钮


保罗·哈德森(Paul Hudson)在他的网站“ Hacking with Swift”上发表了对所讨论的建筑模板的另一篇很好的评论,甚至有人会说一本指南。 它还包含对上述返回按钮问题的一种可能解决方案的简单,直接的说明:协调器(如有必要)声明自己是传递给他的UINavigationController实例的委托,并监视我们感兴趣的事件。

这种方法有一个小缺点:只有NSObject可以是UINavigationController委托。

因此,有一个协调器产生了另一个协调器。 通过调用start()将某种UIViewController添加到UINavigationController堆栈中。 通过单击UINavigationBar上的后退按钮UINavigationBar您需要做的就是让始发协调员知道生成的协调员已经完成了他的工作(“流程”)。 为此,我们引入了另一个委托工具:将委托分配给每个生成的协调器,该接口的接口由生成的协调器实现:

 protocol CoordinatorFlowListener: class { func onFlowFinished(coordinator: Coordinator) } final class MainCoordinator: NSObject, Coordinator { private let dependencies: CoordinatorDependencies private let navigationController: UINavigationController init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager super.init() } func start() { let someCoordinator = SomeCoordinator(navigationController: navigationController, flowListener: self) dependencies.add(someCoordinator) someCoordinator.start() } } extension MainCoordinator: CoordinatorFlowListener { func onFlowFinished(coordinator: Coordinator) { dependencies.remove(coordinator) // ... } } final class SomeCoordinator: NSObject, Coordinator { private weak var flowListener: CoordinatorFlowListener? private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, flowListener: CoordinatorFlowListener) { self.navigationController = navigationController self.flowListener = flowListener } func start() { // ... } } extension SomeCoordinator: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { guard let fromVC = navigationController.transitionCoordinator?.viewController(forKey: .from) else { return } if navigationController.viewControllers.contains(fromVC) { return } if fromVC is SomeViewController { flowListener?.onFlowFinished(coordinator: self) } } } 

在上面的示例中, MainCoordinator不执行任何操作:它只是启动另一个协调器的流程-当然,在现实生活中,它是没有用的。 在我们的应用程序中, MainCoordinator从外部接收数据,根据该数据确定应用程序处于何种状态-授权,未授权等。 -以及需要显示哪个屏幕。 依赖于此,它启动相应协调器的流程。 如果始发协调器完成了工作,则主协调器会通过CoordinatorFlowListener接收有关此信号的信号,例如,启动另一个协调器的流程。

结论


当然,惯用的解决方案有许多缺点(例如对任何问题的任何解决方案)。

是的,您必须使用很多委托,但是它很简单并且有一个单一的方向:从生成到生成(从控制器到协调器,从生成的协调器到生成)。

是的,为了避免内存泄漏,您必须向每个协调器添加一个具有几乎相同实现的委托方法UINavigationController 。 (第一种方法没有这个缺点,但是可以更慷慨地分享其内部关于指定特定协调员的知识。)

但是,这种方法的最大缺点是,在现实生活中,不幸的是,协调员对周围世界的了解比我们想要的要多。 更准确地说,他们将不得不添加依赖于外部条件的逻辑元素,而协调员并未直接意识到这些逻辑元素。 基本上,实际上,这就是在onFlowFinished(coordinator:) start()方法或onFlowFinished(coordinator:)回调时发生的情况。 在这些地方可能发生任何事情,并且始终都是“硬编码”行为:将控制器添加到堆栈中,替换堆栈,然后返回到根控制器-随便什么。 所有这些都不取决于当前控制器的能力,而是取决于外部条件。

不过,该代码“简洁”,简洁,可以很好地使用它,并且浏览代码也更加容易。 在我们看来,有了上述缺点,意识到它们,很可能存在。
感谢您阅读这个地方! 我希望他们学到了一些对自己有用的东西。 如果您突然想要“比我更多”,那么这里是指向我的Twitter链接

Source: https://habr.com/ru/post/zh-CN444038/


All Articles