协调者模式是一种结构化的设计模式,用于组织视图控制器之间的流动逻辑。它包括以下几个部分。
协调者是一个协议,规定了所有具体协调者必须实现的方法和属性。具体来说,它规定了关系属性、子女和路由器。它还规定了展示方法、呈现和驳回。
通过抓住协调者协议,而不是直接抓住具体的协调者,你可以把父协调者和它的子协调者解耦。这使得父协调者可以在一个单一的属性(children)中抓取各种具体的子协调者。
同样地,通过直接抓住一个路由器协议而不是一个具体的路由器,你可以将协调者和它的路由器解耦。
具体的协调者实现了协调者协议。它知道如何创建具体的视图控制器以及视图控制器的显示顺序。
路由器是一个协议,它规定了所有具体路由器必须实现的方法。具体来说,它规定了显示和关闭视图控制器的present和dismiss方法。
具体的路由器知道如何展示视图控制器,但它并不清楚正在展示的是什么,也不知道下一步将展示哪个视图控制器。相反,协调者会告诉路由器要展示哪个视图控制器。
具体的视图控制器是MVC中典型的UIViewController子类。然而,他们不知道其他的视图控制器。相反,每当需要执行转换时,它们就会委托给协调者。
这种模式可以只用于应用程序的一部分,也可以作为一种 "架构模式 "来确定整个应用程序的结构。
在本章中,你会看到这两种模式都在发挥作用。在playground的例子中,你将从一个现有的视图控制器中调用一个协调者,而在教程项目中,你将在整个应用程序中采用这种模式。
使用这种模式可以将视图控制器彼此解耦。唯一能直接了解视图控制器的组件是协调者。
因此,视图控制器的可重用性更强。如果你想在你的应用程序中创建一个新的流程,你只需创建一个新的协调者即可。
打开启动器目录中的AdvancedDesignPatterns.xcworkspace,然后打开协调员页面。
对于这个playground的例子,你将创建一个分步骤的指令流程。你可以将其用于任何指令,如应用程序的设置、首次帮助教程或任何其他分步流程。
为了保持这个例子的简单性和对设计模式的关注,你将创建一个 "如何编码 "的流程,该流程将显示一组视图控制器,只有文本。
按住Option键,左击协调员页面旁边的箭头,展开所有的子文件夹。你会看到已经为你添加了几个文件夹。
控制器包含所有具体的视图控制器。这些都是简单的、普通的视图控制器,已经为你实现了。
Coordinators包含两个文件。Coordinator.swift和HowToCodeCoordinator.swift。如果你打开这两个文件,你会发现它们目前是空的。
同样地,路由器包含两个文件。NavigationRouter.swift和Router.swift。这两个文件目前也都是空的。
这些类型是你需要实现的!
首先,打开Router.swift。这就是你要实现Router协议的地方。
在这个文件中加入以下代码。
import UIKit
public protocol Router: class {
// 1
func present(_ viewController: UIViewController,
animated: Bool)
func present(_ viewController: UIViewController,
animated: Bool,
onDismissed: (()->Void)?)
// 2
func dismiss(animated: Bool)
}
extension Router {
// 3
public func present(_ viewController: UIViewController,
animated: Bool) {
present(viewController,
animated: animated,
onDismissed: nil)
}
}
你在这里声明了一个叫做Router的协议。下面是这个协议的定义。
你首先定义了两个现存的方法。唯一的区别是一个需要一个onDismissed闭包,而另一个不需要。如果提供了这个方法,具体的路由器将在视图控制器被解散时执行onDismissed,例如,在使用UINavigationController的具体路由器中,通过 "pop "动作。
你还要声明dismiss(animated:)。这将解雇整个路由器。根据具体的路由器,这可能会导致弹出到根视图控制器,在父视图控制器上调用dismiss,或任何根据具体路由器的实现而需要的动作。
你最后为present(_:animated:)定义了一个默认的实现。这只是通过传递nil给onDismissed来调用另一个present。
你可能会想,"难道我不需要一个方法来解散单个视图控制器吗?" 令人惊讶的是,你可能不需要!这个playground的例子也不需要。这个playground的例子和教程项目都不需要它。如果你在自己的项目中确实需要这个方法,请随意声明。
你接下来需要实现具体的Router。打开NavigationRouter.swift,并在其中添加以下代码。
import UIKit
// 1
public class NavigationRouter: NSObject {
// 2
private let navigationController: UINavigationController
private let routerRootController: UIViewController?
private var onDismissForViewController:
[UIViewController: (() -> Void)] = [:]
// 3
public init(navigationController: UINavigationController) {
self.navigationController = navigationController
self.routerRootController = navigationController.viewControllers.first
super.init()
}
}
我们来看看这个。
你将NavigationRouter声明为NSObject的一个子类。这是必须的,因为你以后会让它符合UINavigationControllerDelegate。
然后你创建这些实例属性。
navigationController将被用来推送和弹出视图控制器。
routerRootController将被设置为navigationController上的最后一个视图控制器。稍后你将使用它来通过弹出来解散路由器。
onDismissForViewController是一个从UIViewController到ondismiss关闭的映射。你将在后面使用它,在视图控制器被弹出时执行on-dismiss动作。
你会注意到,这还没有实现Router协议。所以,让我们来实现它吧! 在文件的末尾添加以下扩展。
这使得NavigationRouter符合Router。
在present(_:animated:onDismissed:)中,你为给定的viewController设置onDismissed闭包,然后将视图控制器推到navigationController上显示。
在dismiss(animated:)中,你验证routerRootController是否被设置。如果没有,你就简单地在navigationController上调用popToRootViewController(animated:)。否则,你就调用performOnDismissed(for:)来执行on-dismiss动作,然后将routerRootController传给navigationController的popToViewController(_:animated:)。
在performOnDismiss(for:)中,你要保证给定的viewController有一个onDismiss。如果没有,你就提前返回。否则,你就调用onDismiss,并从onDismissForViewController中移除它。
你在这里需要做的最后一件事是让NavigationRouter符合UINavigationController,这样你就可以在用户按下返回按钮时调用on-dismiss动作。在文件的末尾添加以下扩展。
在navigationController(_:didShow:animated:)里面,你从navigationController.transitionCoordinator中获取from view controller,并确认它不包含在navigationController.viewControllers中。这表明视图控制器被弹出,作为回应,你调用 performOnDismissed
来为给定的视图控制器做on-dismiss的动作。
当然,你也需要实际设置NavigationRouter作为navigationController的委托。
在init(navigationController:)的末尾添加以下内容。
navigationController.delegate = self
有了这个,你的NavigationRouter就完成了!
你的下一个任务是创建协调员协议。打开Coordinator.swift,并在其中添加以下内容。
下面是这个的作用。
你为子代和路由器声明关系属性。你将使用这些属性在协调员下一步的扩展中提供默认的实现。
你还声明了present、dismiss和presentChild的必要方法。
你可以为dismiss和presentChild提供合理的默认实现。在文件的末尾添加以下扩展。
下面是这个的作用。
要解雇一个协调者,你只需在其路由器上调用dismiss。这样做的原因是,无论谁提出协调者,都要负责传递一个onDismiss闭包,以进行任何需要的拆除,这将被路由器自动调用。
还记得你是如何在NavigationRouter中写下所有的逻辑来处理弹出和驳回的吗?这就是你这么做的原因
在presentChild中,你只需将给定的儿童追加到儿童中,然后调用child.present。你还通过在孩子的onDismissed动作中调用removeChild(_:)来处理移除孩子的问题,最后,你调用传递到方法本身的所提供的onDismissed。
就像Router没有为单个视图控制器声明一个解雇方法一样,这个协调者也没有为子协调者声明一个解雇方法。道理是一样的:本章的例子不需要它!当然,你可以自由地添加它们。当然,如果有必要的话,请随时在你的应用程序中添加它们。
你需要创建的最后一个类型是具体协调者。打开HowToCodeCoordinator.swift并添加以下代码,暂时忽略你得到的任何编译器错误。
以下是你所做的事情。
首先,你为children和router声明了属性,它们需要分别符合Coordinator和Router。
接下来,你创建了一个名为stepViewControllers的数组,你通过实例化几个StepViewController对象来设置它。这是一个简单的视图控制器,显示一个带有多行标签的按钮。
你将视图控制器的文本设置为Proclaimers乐队的 "I'm Gonna Be (500 miles) "的模仿歌曲歌词。如果你不知道的话,可以谷歌一下。一定要大声唱出这些歌词,特别是如果其他人在附近的话--他们会喜欢的......! 好吧,根据你的歌唱技巧,也许你一个人唱是最好的!
接下来,你为startOverViewController声明一个属性。这将是最后一个显示的视图控制器,并将简单地显示一个按钮来 "重新开始"。
接下来,你创建一个指定的初始化器,接受并设置路由器。
最后,你实现了present(animated:, onDismissed:),这是Coordinator要求的,用来启动流程。
接下来你需要使HowToCodeCoordinator符合StepViewControllerDelegate。把下面的代码添加到文件的末尾;现在继续忽略其他编译器错误。
在stepViewControllerDidPressNext(:)中,你首先尝试获取下一个StepViewController,只要这不是最后一个,就可以通过stepViewController(after:)返回。然后,你把它传递给router.present(:animated:)来显示它。
如果没有下一个StepViewController,你就把startOverViewController传给router.present(_:animated:)。
为了解决其余的编译器错误,你需要让HowToCodeCoordinator符合StartOverViewControllerDelegate。在文件的末尾添加以下代码来实现这一点。
每当startOverViewControllerDidPressStartOver(_:)被调用时,你会调用router.dismiss来结束流程。最终,这将导致返回到启动该流程的第一个视图控制器,因此,用户可以再次启动它。
你已经创建了所有的组件,你已经准备好将它们付诸行动了
打开协调员页面,在代码示例下面添加以下内容。
我们来看看这个。
首先,你创建homeViewController,然后用它来创建navigationController。这将是 "主页 "屏幕。如果这实际上是一个iOS应用程序,这将是该应用程序启动时显示的第一个屏幕。
接下来,你使用navigationController创建路由器,反过来,使用路由器创建协调者。
如果你打开HomeViewController.swift,你会看到它有一个单一的按钮,最终调用其onButtonPressed闭包。在这里,你设置homeViewController.onButtonPressed来告诉协调者呈现,这将开始其流动。
最后,你将PlaygroundPage.current.liveView设置为navigationController,这告诉Xcode在助理编辑器中显示navigationController。
运行playground,你应该看到实时预览显示了这个动作。如果你没有,请选择编辑器并确保实时视图被选中。
点选 "如何编码 "来启动流程。点每一个按钮,直到你看到 "重新开始"。一旦你点了这个按钮,协调员就会被解散,你会再次看到《如何编程》。
确保你在使用这种模式时处理好返回功能。特别是,确保你提供任何所需的拆解代码传递到协调者的onDismiss上(animated:onDismiss:)。
对于非常简单的应用程序来说,协调者模式可能显得有些矫枉过正。你将被要求在前期创建许多额外的类;即具体的协调者和路由器。
对于长期或复杂的应用程序,协调者模式可以帮助你提供所需的结构,增加视图控制器的可重用性。
你将在本章中建立一个名为RayPets的应用程序。这是Ray的一个 "宠物 "项目:为精明的iOS用户提供一个专属的宠物诊所。
打开Finder,导航到你下载本章资源的地方。然后,在Xcode中打开starter ▸ RayPets ▸ RayPets.xcodeproj。
建立并运行,你会看到这个主屏幕。
但是,如果你点击 "安排访问",就不会发生任何事情。
在调查原因之前,先看一下文件的层次结构。特别是,你会发现有一个 "屏幕 "组,它包含已经实现的视图控制器和视图。
这些是MVC中典型的视图控制器。然而,根据协调者模式,它们并不了解视图控制器的转换。相反,每一个都会在需要转换的时候通知其委托人。
最后,看看Screens ▸ Protocols ▸ StoryboardInstantiable.swift。RayPets中的每个视图控制器都符合这个协议。它使从故事板中实例化一个视图控制器变得更容易。特别是,它提供了一个名为instanceFromStoryboard的静态方法,返回Self以从故事板中创建一个视图控制器。
接下来,进入Screens ▸ Home ▸ Controllers ▸ HomeViewController.swift。你会看到didPressScheduleAppointment的IBAction,它是在点击Schedule Visit时被调用的。这反过来又调用homeViewControllerDidPressScheduleAppointment的委托。
然而,应用程序的这一部分还没有被实现。要做到这一点,你需要实现一个新的具体协调者和路由器。
在文件的层次结构中,你也会看到已经有一个协调者组。在这里面,你会发现Coordinator.swift已经从playground的例子中复制了。
你还会看到一个Routers组。这里面有Router.swift,它同样也是从playground范例中复制的。
你将首先实现一个新的具体路由器。在Routers组中,创建一个名为AppDelegateRouter.swift的新文件,并将其内容改为如下。
这个路由器的目的是为了从AppDelegate中抓取窗口。
在present(_:animated:onDismissed:)中,你只需设置window.rootViewController并调用window.makeKeyAndVisible来显示该窗口。
这个路由器将由AppDelegate直接持有,并不意味着可以被抛弃。因此,你只需忽略对dismiss(animated:)的调用。
你接下来需要创建一个协调者来实例化和显示HomeViewController。在Coordinators组中,创建一个名为HomeCoordinator.swift的新文件。将其内容替换为以下内容,暂时忽略编译器的错误。
这个协调者是非常简单的。你为children和router创建了属性,这是协调者协议所要求的,还有一个简单的初始化器来设置router。
在present(animated:onDismissed:)中,你通过调用一个方便的构造方法instantiate(delegate:),将HomeViewController实例化。然后你把它传递给router.present(_:animated:onDismissed:)来显示它。为了解决编译器错误,你需要让HomeCoordinator符合HomeViewControllerDelegate。在文件的末尾添加以下扩展。
你现在只是简单地把这个方法停顿下来。
你还需要实际使用HomeCoordinator。要做到这一点,打开AppDelegate.Swift,用以下内容替换它。
你首先为协调者、路由器和窗口创建了懒惰属性。
然后在application(_:didFinishLaunchingWithOptions:)中,调用coordinator.present来启动HomeCoordinator的流程。
构建并运行,你会看到应用程序显示HomeViewController,就像以前一样。然而,你现在已经准备好在整个应用程序中实现协调者模式了
特别是,接下来你将专注于实现一个新的协调者,用于安排宠物约会,以响应按排访问。
打开Models ▸ PetAppointment.swift,你会看到一个模型和相关的构建器已经被定义了。宠物约会和宠物约会建构器(PetAppointmentBuilder)。
你将创建一个新的协调者,以便从用户那里收集PetAppointmentBuilder的输入。在协调者组中创建一个名为PetAppointmentBuilderCoordinator.swift的新文件,并将其内容改为以下内容,暂时忽略编译器的错误。
PetAppointmentBuilderCoordinator有一个builder的属性,你将用它来设置来自用户的输入,以及根据Coordinator的协议,为儿童和路由器提供所需的属性。
在present(animated:onDismissed:)中,你通过instantiate(delegate:)实例化一个SelectVisitTypeViewController,然后将其传递给router.present(_:animated:onDismissed)。
听起来很熟悉,对吗?这与HomeCoordinator非常相似,这也是你会看到的使用协调者的重复模式:你实例化一个视图控制器,将其传递给路由器来呈现,并通过委托回调接收反馈。
因此,你需要使PetAppointmentBuilderCoordinator符合
SelectVisitTypeViewControllerDelegate。在文件末尾添加以下扩展的末尾,同样暂时忽略编译器错误。
下面是这个的作用。
在selectVisitTypeViewController(_:didSelect:)中,你首先设置builder.visitType。
然后你打开所选的visitType。
如果访问类型是好的,你就调用presentNoAppointmentViewController()来显示NoAppointmentRequiredViewController。
如果是生病了,你就调用presentSelectPainLevelCoordinator()来显示一个SelectPainLevelViewController。
NoAppointmentRequiredViewController和SelectPainLevelViewController都需要各自的委托,但PetAppointmentBuilderCoordinator还不符合它们的委托协议。
接下来,在文件的末尾添加以下内容,使PetAppointmentBuilderCoordinator符合SelectPainLevelViewControllerDelegate。
这与你处理前一个委托人互动的方式相似。
在selectPainLevelViewController(_:didSelect:)中,你首先设置builder.painLevel。
然后你打开所选的疼痛级别。
如果情况符合无或少,你就调用presentFakingItViewController()来显示一个FakingItViewController。
如果情况符合中度、重度或最坏的可能性,你可以presentNoAppointmentViewController()来显示一个NoAppointmentRequiredViewController。
因此,你需要实现另一个委托协议。FakingItViewControllerDelegate。幸运的是,这个很简单。在文件的结尾处添加以下代码。
这种互动是非常直接的。
在fakingItViewControllerPressedIsFake(_:)中,你只需调用router.dismiss(animated:)来退出协调者的流程。
在fakingItViewControllerPressedNotFake(_:)中,你再次调用
presentNoAppointmentViewController()来显示一个NoAppointmentRequiredViewController。
等一下--所以无论用户做什么,他们最终都会看到NoAppointmentRequiredViewController。这到底是怎么回事呢?
我和Ray谈过这个问题,他说这是一种营销策略,目的是让客户到公司来......要么就是,有人没有为这个例子的应用写一个后台。因此,似乎没有地方可以真正提交这些数据。你现在要做什么呢?
不怕!就像在现实生活中,当后方的人不在的时候,你会发现他们在做什么。就像在现实生活中,当后端没有准备好时,你就假扮它! 因此,应用程序最终会显示NoAppointmentRequiredViewController,不管之前的选择如何。
你只需要再实现一个协议就可以了。NoAppointmentRequiredViewControllerDelegate。把这段代码添加到这个文件的结尾。
作为对noAppointmentViewControllerDidPressOkay(_:)的回应,你只需调用router.dismiss(animated:)来退出应用程序的流程。
很好! 你现在只需要一个具体的路由器来和这个协调者一起使用。
你可能想知道,"我难道不能直接使用playground示例中的NavigationRouter吗?"
NavigationRouter需要一个现有的UINavigationController并将
视图控制器到它上面。由于安排访问的流程与主页的流程不同,Ray真的希望这能以模式呈现,而NavigationRouter并不是用来做这个的。
相反,你将创建一个新的路由器,创建一个新的UINavigationController,并使用现有的parentViewController来展示它,以支持这个用例。
在Routers组中,创建一个名为ModalNavigationRouter.swift的新文件,并将其内容改为以下内容。
以下是你所做的事情。
首先,你将ModalNavigationRouter声明为NSObject的一个子类。成为NSObject的子类是必须的,因为你以后要让它符合UINavigationControllerDelegate。
接下来,你为parentViewController、navigationController和onDismissForViewController创建实例属性。
最后,你声明一个初始化器,接受parentViewController。
接下来,你需要让ModalNavigationRouter符合Router的要求。在文件的底部添加以下代码。
下面是这个的作用。
然后检查navigationController是否没有任何视图控制器。这意味着你需要适度呈现导航控制器,你可以通过调用presentModally(_:animated:)来实现。否则,你就对navigationController调用pushViewController。
在presentModally(_:animated:)中,你首先将视图控制器传递给addCancelButton(to:),以便在视图控制器上设置一个取消按钮。如果该按钮被点击,它将调用cancelPressed(),执行ondismiss动作,最终调用dismiss(animated:)。
接下来,还是在presentModally(_:animated:)中,你用传入的viewController来设置navigationController的viewController。然后,我们在parenterViewController上调用present,以便以模态呈现navigationController。
在dismiss(animated:)中,你通过navigationController.viewControllers.first调用performOnDismissed。然后你告诉parentViewController,解散它所呈现的视图控制器,也就是navigationController。
performOnDismissed(for:) 与NavigationRouter中的方法完全相同。它检查视图控制器是否有一个现有的onDismiss闭包,如果发现的话就执行它,最后从onDismissForViewController中删除闭包。
太好了! 完成ModalNavigationRouter的唯一剩余任务是使其符合UINavigationControllerDelegate。为此,在文件的末尾添加以下内容。
这就像playground上的NavigationRouter的实现一样。你检查从视图控制器是否被弹出,如果是,就调用performOnDismissed来执行其on-dismiss闭包。
当然,你也需要将 navigationController.delegate 设置为 ModalNavigationRouter。在init(parentViewController:)的结尾处添加以下内容,就在关闭方法的括号前。
navigationController.delegate = self
太棒了! 你已经创建了所有必要的部分来显示预约访问的流程。你现在只需要把它们全部放在一起。
还记得HomeCoordinator中的TO-DO吗?是的,这就是你要触发宠物预约建造者(PetAppointmentBuilderCoordinator)流程的地方。
打开HomeCoordinator.swift,将TO-DO注释替换为以下内容。
let router = ModalNavigationRouter(parentViewController: viewController)
let coordinator = PetAppointmentBuilderCoordinator(router: router)
presentChild(coordinator, animated: true)
你在这里创建了一个新的ModalNavigationRouter,然后你又用它来创建一个新的PetAppointmentBuilderCoordinator,并把它传递给presentChild(_:animated:)来启动这个流程。
构建并运行,然后点选访问时间表,就可以看到正在运行的访问安排流程。
你在本章中了解了协调者模式。下面是它的关键点。
协调者模式组织了视图控制器之间的流程逻辑。它涉及到协调者协议、具体协调者、路由器协议、具体路由器和视图控制器。
协调者定义了所有具体协调者必须实现的方法和属性。
具体协调者知道如何创建具体的视图控制器和它们的顺序。
路由器规定了所有具体的路由器必须实现的方法。
具体的路由器知道如何呈现视图控制器。
具体的视图控制器是典型的视图控制器,但它们不知道其他视图控制器。
这种模式可以只在应用程序的一部分采用,也可以在整个应用程序中使用。
协调者模式是组织长期或非常复杂的应用程序的一个伟大模式。它是由Soroush Khanlou首次介绍给iOS社区的。你可以在他的博文中了解更多关于这种模式的根源。
还有其他一些与Coordinator类似的结构和架构模式。其中一个例子是VIPER,它进一步将对象按责任分开。你可以在objc.io的这篇报道中了解更多关于它的信息。
上一章 | 目录 | 我就是最后一章 |
---|