协调器模式问题以及RouteComposer与它有什么关系

我继续关于我们使用的RouteComposer库的系列文章,今天,我想谈谈Coordinator模式。 通过讨论其中一篇有关模式的文章,我被提示写这篇文章,这里的协调员是Habr。


不久前推出的Coordinator模式在iOS开发人员中越来越受欢迎,总的来说,这很清楚。 因为UIKit提供的开箱即用的工具并不是很普遍。


图片


我已经提出了在堆栈上构成控制器视图的方式的碎片化问题,并且为了避免重复,您可以在此处阅读有关内容。


说实话 在某个时候,Epole意识到,通过将控制器放在应用程序开发中心中,她没有提供任何明智的方式来在它们之间创建或传输数据,并且将解决此问题的解决方案委托给开发人员后,它已经由Xcode甚至UISearchConnroller开发人员自动完成了,在某些时候向我们介绍了情节提要和segues。 然后,Epolus意识到她自己编写了由2个屏幕组成的应用程序,并且在下一次迭代中,她建议将情节提要板分成几个组件,因为情节提要板达到一定大小时Xcode开始崩溃。 Segues伴随着这个概念而发生了变化,在几次迭代中彼此之间不太兼容。 他们的支持紧密地缝在了庞大的UIViewController类中,最后,我们得到了我们所得到的。 这是:


 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "showDetail" { if let indexPath = tableView.indexPathForSelectedRow { let object = objects[indexPath.row] as! NSDate let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController controller.detailItem = object controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem controller.navigationItem.leftItemsSupplementBackButton = true } } } 

此代码块中强制广播的数量令人惊叹,情节提要中的字符串常量也是如此,它们可以跟踪哪个Xcode根本不提供任何手段。 丝毫希望在导航过程中进行某些更改,这将使您能够毫不费力地编译项目,并且在运行时发生崩溃的情况下,如果没有Xcode的任何警告,它将崩溃。 最终结果就是这样的所见即所得。 所见即所得。


您可以在情节提要中争论这些灰色箭头的魅力很长时间,该故事板本来可以显示某人在屏幕之间的联系,但是,正如我的实践所示,我故意采访了几家来自不同公司的熟悉的开发人员,只要该项目扩展到5-6个屏幕以上,人们就会尝试找到更可靠的解决方案,最后开始将视图控制器堆栈的结构保留在我的脑海中。 而且,如果添加了对iPad和其他导航模型的支持或对推送的支持,那么那里的一切都是令人难过的。


从那时起,已经进行了数种尝试来解决该问题,其中一些尝试导致了单独的框架,而另一些尝试采用了不同的体系结构模式,因为在视图控制器内部创建视图控制器使此庞大而笨拙的代码段更加复杂。


让我们回到协调器模式。 出于明显的原因,您不会在Wikipedia上找到它的描述,因为它不是标准的编程/设计模式。 相反,它是一种抽象,建议将所有这些“丑陋的”代码隐藏在后台,以便在堆栈上创建和插入新的控制器扭曲,保存到控制器容器的链接以及在控制器之间推送数据。 描述此过程的最合适的文章是raywenderlich.com上的文章。 在2015年NSSpain会议上,当公众被告知时,它开始变得流行。 在这里这里可以找到更详细的信息。


在继续之前,我将简要描述其组成。


所有解释中的协调器模式都大致适合此图片:



也就是说,协调器是一个协议


 protocol Coordinator { func start() } 

并且所有丑陋的代码都应该隐藏在start函数中。 此外,协调器还可以具有指向子协调器的链接,也就是说,它们具有一定的组合能力,例如,您可以将一个实现替换为另一个实现。 也就是说,听起来很优雅。


但是,无罪现象很快就会开始:


  1. 一些实现建议将协调器从某种生成方式转变为更合理的方式,监视控制器堆栈,并使其成为容器的委托 ,例如UINavigationController ,以处理按下“后退”按钮或向后滑动并删除子协调器。 出于自然原因,只有一个对象可以是委托,这限制了对容器本身的控制,并导致以下事实:该逻辑要么由协调器承担,要么需要将该逻辑进一步委托给列表中的其他人。
  2. 创建下一个控制器的逻辑通常取决于业务逻辑 。 例如,要转到下一个屏幕,用户必须登录到系统。 显然,这是一个异步过程,其中包括使用登录表单生成一些中间屏幕,登录过程本身能否成功结束。 为了避免将协调器转换为大规模协调器(类似于大规模视图控制器),我们需要分解。 也就是说,实际上,您需要创建一个协调器Coordinator。
  3. 协调器面临的另一个问题是,它们实际上是容器视图控制器(如UINavigationControllerUITabBarController等)的包装。 有人应该提供到这些控制器的链接 。 如果与儿童协调员之间的一切都不那么清楚,那么对于连锁店的最初协调员,并非一切都那么简单。 另外,在更改导航(例如进行A / B测试)时,此类协调器的重构和调整会导致单独的头痛。 特别是如果容器的类型发生变化。
  4. 当应用程序开始支持生成视图控制器的外部事件时,所有这些都变得更加复杂。 例如推送通知或通用链接(用户单击字母中的链接,然后在相应的应用程序屏幕中继续)。 这里出现了其他不确定性,而协调器模式没有确切的答案。 您需要确切知道用​​户当前在哪个屏幕上,以便向他显示外部事件请求的下一个屏幕。
    最简单的示例是一个由三个屏幕组成的聊天应用程序-聊天列表,将聊天本身推送到聊天列表控制器的导航中以及以模态显示的设置屏幕。 当用户接收到推送通知并在其上点击时,他可以在这些屏幕之一上。 然后不确定性就开始了,如果他在聊天列表中,则需要与该特定用户开始聊天;如果他已经在聊天中,则需要进行切换;如果他已经在与该用户聊天中,则不执行任何操作并更新(如果该用户处于在线状态)设置屏幕-它显然需要关闭并按照前面的步骤进行操作。 还是可能无法关闭,只是在设置上模态显示聊天? 如果设置在另一个选项卡中,而不是模态? 这些( if/else开始散布在协调器中,或者以一块意大利面条的形式转到另一个巨型协调器。 另外,它要么是控制器视图堆栈上的活动迭代,要么是确定用户当前所在的位置的尝试,要么是尝试构建某种监视其状态的应用程序,但这并不是一件容易的事,仅基于视图控制器堆栈本身的性质。
  5. 蛋糕上的樱桃是UIKit故障 。 一个简单的示例:一个UITabBarController ,在第二个选项卡中带有一个UINavigationController ,以及另一个UIViewController 。 第一个选项卡中的用户会导致某些事件,该事件需要切换选项卡并将另一个视图控制器UINavigationController入其UINavigationController 。 所有这些都需要按照这样的顺序进行。 如果用户在此之前从未打开过第二个选项卡,并且没有在viewDidLoad上调用UINavigationController viewDidLoadpush方法将不起作用,在控制台中仅留下不清楚的消息。 也就是说,在此示例中,不能简单地使协调器成为事件的侦听器,它们必须按一定顺序工作。 因此,他们必须彼此了解。 这已经与协调器模式的第一个陈述相矛盾,即协调器对生成协调器一无所知,仅与子协调器建立联系。 并且也限制了它们的互换性。

该列表可以继续,但是总的来说,很明显,协调器模式是一个相当有限的,可扩展性差的解决方案。 如果您不戴粉红色眼镜观看它,那么它是一种将一部分逻辑分解为另一类的方法,该逻辑通常是在大量UIViewController内部编写的。 所有试图使它不仅仅是一个生成工厂,并在那里引入其他逻辑的尝试都没有很好地结束。


值得注意的是,有一些基于这种模式的库,以一种或另一种方式,可以部分缓解上述缺点。 我会提到XCoordinatorRxFlow


我们做了什么?


在参与我们从另一个团队获得支持和开发的项目之后,在VIPER架构方法中使用了协调器及其简化的“曾祖母” 路由器 ,我们回滚到在公司先前的大型项目中运行良好的方法。 这种方法没有名称。 它位于表面上。 当我们有空闲时间时,它被编译到一个单独的RouteComposer库中,该库完全替代了协调器,并被证明更加灵活。


这是什么方法? 这样,为了依赖堆栈(树),我按原样扭曲了控制器。 为了不创建不必要的实体,需要对其进行监视。 不要保存或跟踪条件。


让我们仔细地看一下UIKit实体,并尝试找出最重要的内容以及可以使用的内容:


  1. 控制器堆栈是一棵树。 有一个具有子视图控制器的根视图控制器。 模态呈现的视图控制器是子视图控制器的一种特殊情况,因为它们也绑定到生成的视图控制器。 开箱即用。
  2. 我需要创建控制器的实体。 它们都有不同的构造函数;可以使用Xib文件或Storyboard创建它们。 它们具有不同的输入参数。 但是它们团结在一起,需要创建它们。 因此,在这里我们可以使用Factory模式,该模式知道如何创建所需的视图控制器。 每个工厂都很容易进行全面的单元测试,并且彼此独立。
  3. 我们将视图控制器分为2类:1.仅查看控制器,2. 容器视图控制器(Container View Controller) 。 容器视图控制器与普通视图控制器的不同之处在于,它们可以包含子视图控制器,也可以包含容器或简单的子视图控制器。 这样的视图控制器可以直接使用: UINavigationControllerUITabBarController等等,但是也可以由用户创建。 如果忽略它,我们可以在所有容器中找到以下属性:1.它们具有包含的所有控制器的列表。 2.当前可以看到一个或多个控制器。 3.可能会要求他们使这些控制器之一可见。 这就是UIKit控制器所能做的 。 他们只是有不同的方法。 但是只有3个任务。
  4. 要嵌入工厂创建的视图控制器,控制器的父视图方法是UINavigationController.pushViewController(...)UITabBarController.selectedViewController = ...UIViewController.present(...)等。 您可能会注意到,始终需要2个视图控制器,其中一个已经在堆栈中,而另一个则需要嵌入在堆栈中。 用包装器将其包装,然后将其称为Action(Action) 。 每个动作都易于通过全面的单元测试来涵盖,并且每个动作都相互独立。
  5. 从上面可以看出,使用准备好的实体,您可以构建配置链工厂->操作->工厂->操作->工厂 ,完成后,您可以构建任何复杂程度的控制器视图树。 您只需要指定入口点。 这些输入点通常是UIWindow拥有的rootViewController或当前视图控制器,它是树的最极端的分支。 也就是说,这样的配置正确编写为: 启动ViewController-> Action-> Factory-> ...-> Factory
  6. 除了配置之外,您还将需要一些知道如何启动和构建所提供配置的实体。 我们将其称为Router 。 它没有状态,不包含任何链接。 它具有一种将配置传递给的方法,并且它顺序执行配置步骤。
  7. 通过将Interceptors类添加到配置链,为路由器增加责任。 拦截器有3种类型:1.在开始导航之前启动。 我们删除系统中的用户身份验证任务以及其中的其他异步任务。 2.在创建视图控制器时运行以设置值。 3.在导航和执行各种分析任务之后执行。 每个实体都容易被单元测试覆盖,并且不知道如何在配置中使用它。 她只有一项责任,她可以履行。 也就是说,复杂导航的配置可能看起来像[导航前任务...]->启动ViewController->操作->(Factory + [ContextTask ...])-> ...->(Factory + [ContextTask ...])-> [Post NavigationTask ...] 。 也就是说,所有任务将由路由器依次执行,依次执行小的,易于阅读的原子实体。
  8. 配置无法解决的最后一项任务仍然存在-这是当前应用程序的状态。 如果我们不需要构建整个配置链,而是仅构建其中的一部分,因为用户部分通过了该怎么办? 视图控制器树始终可以明确回答这个问题。 因为如果链的一部分已经建立,那么它已经在树中了。 这意味着,如果链中的每个工厂都可以回答是否建厂的问题,那么路由器将能够了解链中哪一部分需要完成。 当然,这不是工厂的任务,因此引入了另一个原子实体-Finder,并且任何配置都如下所示: [预导航任务...]->启动ViewController->操作->(Finder / Factory + [ContextTask ...]) -> ...->(Finder / Factory + [ContextTask ...])-> [Post NavigationTask ...] 。 如果路由器从头开始读取它,那么Finders之一将告诉他它已被构建,并且从这一点开始,路由器将开始重新构建链。 如果没有一个人发现自己在树中,那么您需要从初始控制器构建整个链。
    图片
  9. 该配置必须是强类型的。 因此,每个实体只能使用一种类型的控制器视图;一种类型的数据和配置完全取决于swift与关联类型一起工作的能力。 我们要依赖编译器,而不是运行时。 开发人员可以有意地减弱键入,但反之则不能。

这种配置的示例:


 let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) .add(LoginInterceptor<UUID>()) // Have to specify the context type till https://bugs.swift.org/browse/SR-8719 is fixed .add(ProductViewControllerContextTask()) .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) .using(UINavigationController.push()) .from(NavigationControllerStep()) .using(GeneralActions.presentModally()) .from(GeneralStep.current()) .assemble() 

上面描述的项目涵盖了整个库并描述了该方法。 我们剩下的就是提供当用户单击按钮或发生外部事件时路由器将执行的链配置。 如果这些设备是不同类型的设备(例如iPhone或iPad),那么我们将使用多态性提供不同的过渡配置。 如果我们有A / B测试,那也是一样。 在开始导航时,我们不需要考虑应用程序的状态,我们需要确保最初正确地编写了配置,并且可以确定路由器将以某种方式构建它。


所描述的方法比某种抽象或模式要复杂得多,但是我们还没有遇到还不够的问题。 当然, RouteComposer需要研究和理解其工作方式。 但是,这比学习AutoLayout或RunLoop的基础要容易得多。 没有更高的数学。


该库以及提供给它的路由器的实现,在运行时不使用任何客观技巧,而是完全遵循所有Cocoa Touch概念,仅有助于将合成过程分解为多个步骤并按给定顺序执行。 该库已通过iOS 9至12版进行了测试。


可以在以前的文章中找到更多详细信息:
UIViewController的组成以及它们之间的导航(不仅如此)/ Geek Magazine
使用RouteComposer / Geek Magazine的UIViewControllers的配置示例


谢谢您的关注。 我很乐意在评论中回答问题。

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


All Articles