Coordinator(协调器)——一种架构性导航模式,将屏幕间切换的逻辑从ViewController转移到独立的类中。该模式由Soroush Khanlou于2015年提出,并在iOS社区中广泛传播。Coordinator管理应用程序的流程:创建并显示ViewController,在屏幕之间传输数据,并处理流程的完成。该模式通过将导航从控制器中分离来解决Massive View Controller问题。更多详情——请参阅Coordinator原创文章。
要点
Coordinator——一种在iOS应用程序中接管导航责任的模式。在标准的UIKit中,ViewController自行管理切换:present、push、show segue——所有导航方法都从UIViewController调用。Coordinator将此逻辑分离出来:ViewController报告事件(例如,“用户点击了登录按钮”),Coordinator决定接下来显示哪个屏幕。ViewController只保留UI逻辑,并将导航委托给协调器。
模式结构——带有start()和finish()方法的CoordinatorProtocol。start()——流程开始:创建第一个ViewController并显示。finish()——流程完成并通知父协调器。Router——UINavigationController(或UISplitViewController)的包装器,提供show、push、pop、dismiss方法。协调器不直接与UINavigationController交互——仅通过Router。这使得可以测试导航并切换UI框架。
| 组件 | 角色 | 示例 |
|---|---|---|
| Coordinator | 管理导航流程 | AuthCoordinator, ProfileCoordinator |
| Router | UINavigationController的抽象 | push, present, pop, dismiss |
| ViewController | UI + 将事件委托给Coordinator | LoginViewController.delegate |
Coordinator解决的问题——Massive View Controller(导航——控制器膨胀的常见原因)。在标准的UIKit中,ViewController包含prepareForSegue、导航委托、unwind segues处理。Coordinator消除了这些问题。Storyboard中的Segue——屏幕之间的静态连接,Coordinator提供带条件的动态导航。导航测试成为可能:可以在没有UI的情况下测试Coordinator,通过检查Router调用的顺序。
Swift中的基础Coordinator——一个带有Router关联类型和start/finish方法的协议。Router——抽象UINavigationController的协议。Router的具体实现包装UINavigationController并将方法委托给它。Coordinator在init中接收Router并将其用于导航。子协调器存储在childCoordinators数组中,用于生命周期管理。
// Router — 导航抽象
protocol RouterProtocol: AnyObject {
func push(_ viewController: UIViewController, animated: Bool)
func pop(animated: Bool)
func present(_ viewController: UIViewController, animated: Bool)
func dismiss(animated: Bool)
}
final class NavigationRouter: RouterProtocol {
private let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func push(_ vc: UIViewController, animated: Bool) {
navigationController.pushViewController(vc, animated: animated)
}
func pop(animated: Bool) {
navigationController.popViewController(animated: animated)
}
func present(_ vc: UIViewController, animated: Bool) {
navigationController.present(vc, animated: animated)
}
func dismiss(animated: Bool) {
navigationController.dismiss(animated: animated)
}
}
// Coordinator — 流程管理
protocol CoordinatorProtocol: AnyObject {
var childCoordinators: [CoordinatorProtocol] { get set }
var router: RouterProtocol { get }
func start()
func finish()
}
class AuthCoordinator: CoordinatorProtocol {
var childCoordinators: [CoordinatorProtocol] = []
let router: RouterProtocol
init(router: RouterProtocol) {
self.router = router
}
func start() {
let loginVC = LoginViewController()
loginVC.onLogin = { [weak self] in
self?.showHome()
}
router.push(loginVC, animated: true)
}
private func showHome() {
let homeCoordinator = HomeCoordinator(router: router)
childCoordinators.append(homeCoordinator)
homeCoordinator.start()
}
func finish() {
childCoordinators.removeAll()
router.pop(animated: true)
}
}
在AppDelegate/SceneDelegate中创建Coordinator——AppDelegate或SceneDelegate创建UINavigationController,将其包装到NavigationRouter中,创建根Coordinator(AppCoordinator)并调用start()。AppCoordinator根据应用程序状态决定显示引导、登录还是主屏幕。Coordinator——导航的唯一入口点,ViewController不知道其他屏幕。
协调器层次结构——AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator。子协调器由父级创建并存储在childCoordinators数组中。当子协调器完成工作时,它调用父级的finish(),父级将其从childCoordinators中移除。这防止了内存泄漏:Coordinator对ViewController(通过Router)有强引用,如果不从childCoordinators中移除,对象将不会被释放。
// 用于Coordinator -> Parent通信的委托
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — 父级
class AppCoordinator: AuthCoordinatorDelegate {
func startAuthFlow() {
let authCoordinator = AuthCoordinator(router: router)
authCoordinator.delegate = self
childCoordinators.append(authCoordinator)
authCoordinator.start()
}
func authCoordinatorFinished(_ coordinator: AuthCoordinator) {
childCoordinators.removeAll { $0 is AuthCoordinator }
startMainFlow()
}
}
管理childCoordinators——从数组中移除Coordinator是释放它的唯一方法。如果忘记移除已完成的Coordinator,它会与ViewController一起留在内存中。推荐使用:父级的didMove(toParent:)、完成时的回调或Combine publisher进行自动移除。Coordinator模式不指定通知机制——委托、closure或Combine——选择权在开发人员。
通过委托传输数据——子Coordinator定义一个委托协议,其中包含传输结果的方法:func authCoordinator(_:didLoginWith user: User)。父级实现该协议,并在子流程完成时接收数据。这是类型安全的且明确的。缺点:每个子Coordinator都需要编写单独的协议。对于有10+个Coordinator的项目,这会导致文件数量增加。
通过Result类型传输数据——finish方法接收Result
| 方法 | 优点 | 缺点 |
|---|---|---|
| Delegate | 类型安全,明确,独立协议 | 协议多,样板代码多 |
| Closure | 紧凑,文件少 | 难以调试retain cycle |
| Combine/Rx | 响应式,易于组合 | 依赖库,调试较难 |
共享数据层——协调器不直接传输数据,而是使用共享服务/存储库。AuthCoordinator将token保存在Keychain/UserDefaults中,ProfileCoordinator从那里读取。协调器通过共享状态(依赖注入容器)进行通信,而不是通过直接调用。这降低了协调器之间的耦合度,但产生了对共享状态的隐式依赖。
Coordinator vs Router——Router是抽象UINavigationController的Coordinator组件。Coordinator负责流程(显示哪个屏幕),Router负责机制(如何显示:push/present)。Router是“如何”,Coordinator是“什么”。Router可以在没有Coordinator的情况下使用(例如,Navigator单例),但没有Router的Coordinator只是具有不同抽象的ViewController。通常两种模式一起使用。
Coordinator vs VIPER——VIPER有一个负责导航的Wireframe组件——类似于Coordinator。在VIPER中,Wireframe是模块的一部分,Coordinator是模块之上的独立层。VIPER模块(View-Interactor-Presenter-Entity-Router)将导航作为模块的一部分。Coordinator相对于模块是外部的:它创建并连接模块,但不属于模块的组成部分。Coordinator在不同的流程中重用屏幕时更灵活。
MVVM-C——MVVM与Coordinator的扩展。ViewModel不直接知道Coordinator——ViewController通过ViewModel委托导航,ViewModel通过协议调用协调器。MVVM-C——使用SwiftUI的iOS项目的标准方法:Coordinator管理NavigationStack或fullScreenCover,ViewModel通过发布状态来调用协调器。Apple不推荐在SwiftUI中使用Coordinator——NavigationStack和NavigationPath是内置的导航机制。
// MVVM-C:ViewModel通过协议调用Coordinator
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// 逻辑...
navigation?.showMainScreen()
}
}
常见问题
对于SwiftUI,内置导航(NavigationStack、NavigationPath)通常可以替代Coordinator。Apple推荐基于路径的导航。Coordinator对于具有深层条件的复杂流程(根据角色决定引导-登录-主屏幕)有意义。对于简单的SwiftUI应用程序,Coordinator是多余的——请使用NavigationPath。
不,它们是不同的模式。Coordinator管理导航流程:决定显示哪个屏幕,创建ViewController并连接它们。Router——UINavigationController的抽象:push、present、pop、dismiss。Coordinator使用Router来执行导航。在某些实现中,Router包含Coordinator的逻辑(Router-per-screen),但这是对原始模式的偏离。
两个主要泄漏点:childCoordinators(父级持有子级,忘记移除)和Router(UINavigationController持有ViewController)。解决方案:始终在finish()时从数组中移除子Coordinator。对delegate使用weak引用。对于Router——如果已经在window层次结构中,不要持有对UINavigationController的强引用。测试Coordinator的deinit。
对于具有3-5个屏幕的应用程序,Coordinator是多余的——segue或简单的navigationController.pushViewController更简单。对于使用NavigationStack的SwiftUI应用程序——也是多余的。Coordinator适用于具有15+个屏幕、复杂流程(带有分支的引导、带密码恢复的授权)和混合UIKit/SwiftUI的项目。
Mock Router——检查哪些方法被调用以及使用什么参数。检查childCoordinators:start()后数组不为空,finish()后为空。Coordinator在没有UI的情况下进行测试:Router是一个协议,其mock不需要UIKit。对于异步流程,使用XCTestExpectation。在Android中类似——使用mock导航测试NavigationController和NavHost。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。