AnyRouter
替换为UnownedRouter
(在viewControllers、viewModels或对父协调器的引用中)或在您的AppDelegate
或对子协调器的引用中使用StrongRouter
。此外,根ViewController现在是在初始化器中注入的,而不是在Coordinator.generateRootViewController
方法中创建的。
“应用如何从 一个视图控制器过渡 到另一个?” 这个问题在iOS开发中很常见,也很令人困惑。有各种各样的答案,因为每个架构都有不同的实施变体。有些是在视图控制器实现内部完成的,而有些则使用路由/协调器,这是一个连接视图模型的对象。
为了更好地回答这个问题,我们正在构建XCoordinator,一个基于协调器模式的导航框架。它对于实施MVVM-C、Model-View-ViewModel-Coordinator特别有用。
🏃♂️ 入门
创建一个枚举,包含特定流程的所有导航路径,即一组紧密相连的场景的导航路径。(何时创建一个Route/Coordinator
取决于您。按照我们的经验,每当需要一个新根视图控制器,例如一个新的导航控制器
或标签栏控制器
时,就创建一个新的Route/Coordinator
。)
虽然Route
描述了在流程中可以触发哪些路由,但Coordinator
负责根据触发路由来准备过渡。因此,我们可以为同一路由准备多个协调器,这些协调器在执行的过渡上有所不同。
以下示例中,我们创建了UserListRoute
枚举,用于定义我们应用程序流程的触发器。UserListRoute
提供打开主页、显示用户列表、打开特定用户和注销的路径。UserListCoordinator
负责为触发路由准备过渡。当显示UserListCoordinator
时,它触发.home
路由以显示HomeViewController
。
enum UserListRoute: Route {
case home
case users
case user(String)
case registerUsersPeek(from: Container)
case logout
}
class UserListCoordinator: NavigationCoordinator<UserListRoute> {
init() {
super.init(initialRoute: .home)
}
override func prepareTransition(for route: UserListRoute) -> NavigationTransition {
switch route {
case .home:
let viewController = HomeViewController.instantiateFromNib()
let viewModel = HomeViewModelImpl(router: unownedRouter)
viewController.bind(to: viewModel)
return .push(viewController)
case .users:
let viewController = UsersViewController.instantiateFromNib()
let viewModel = UsersViewModelImpl(router: unownedRouter)
viewController.bind(to: viewModel)
return .push(viewController, animation: .interactiveFade)
case .user(let username):
let coordinator = UserCoordinator(user: username)
return .present(coordinator, animation: .default)
case .registerUsersPeek(let source):
return registerPeek(for: source, route: .users)
case .logout:
return .dismiss()
}
}
}
路由是从协调器或ViewModel内部触发的。以下将描述如何在ViewModel内部触发路由。当前流程的路由器被注入到ViewModel中。
class HomeViewModel {
let router: UnownedRouter<HomeRoute>
init(router: UnownedRouter<HomeRoute>) {
self.router = router
}
/* ... */
func usersButtonPressed() {
router.trigger(.users)
}
}
🏗 使用XCoordinator组织应用程序结构
一般来说,应用程序的结构是通过嵌套协调器和视图控制器定义的。每当您的应用程序转换到不同的流程时,您都可以在任意时刻将过渡到不同的协调器。在一个流程中,我们在视图控制器之间进行过渡。
示例:在 UserListCoordinator.prepareTransition(for:)
方法中,每当触发 UserListRoute.user
路由时,我们会从 UserListRoute
跳转到 UserRoute
。通过在 UserListRoute.logout
中弹出 viewController,我们还可以切换回之前的流程——在本例中为 HomeRoute
。
为实现此功能,每个 Coordinator 都有自己的 rootViewController
。对于 NavigationCoordinator
,它是一个 UINavigationController
;对于 TabBarCoordinator
,它是一个 UITabBarController
等等。在过渡到 Coordinator/Router 时,会将这个 rootViewController
作为目标 viewController。
🏁 从 App 启动使用 XCoordinator
要从小程序的启动时使用 coordinators,请确保在 AppDelegate.swift
中以编程方式创建程序的 window
(别忘了从 Info.plist
中移除 Main Storyboard 文件名称
)。然后,在 AppDelegate.didFinishLaunching
中将协调者设置为 window 视层级结构的根。请确保持有对程序初始协调器或 strongRouter
引用的强引用。
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
let window: UIWindow! = UIWindow()
let router = AppCoordinator().strongRouter
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
router.setRoot(for: window)
return true
}
}
🤸♂️ 额外功能
对于更高级的使用,XCoordinator 提供了许多自定义选项。我们介绍了自定义动画过渡和深链接。此外,还描述了用于 RxSwift/Combine 中的响应式编程扩展和拆分大型路由的选项。
🌗 自定义过渡
自定义动画过渡定义了展示和消失的动画。你可以在协调器中的 prepareTransition(for:)
方法中指定几种常见过渡的 Animation
对象,例如 present
、dismiss
、push
和 pop
。指定无动画(nil
)则会取消覆盖之前设置的动画。使用 Animation.default
将先前设置的动画重置为 UIKit 提供的默认动画。
class UsersCoordinator: NavigationCoordinator<UserRoute> {
/* ... */
override func prepareTransition(for route: UserRoute) -> NavigationTransition {
switch route {
case .user(let name):
let animation = Animation(
presentationAnimation: YourAwesomePresentationTransitionAnimation(),
dismissalAnimation: YourAwesomeDismissalTransitionAnimation()
)
let viewController = UserViewController.instantiateFromNib()
let viewModel = UserViewModelImpl(name: name, router: unownedRouter)
viewController.bind(to: viewModel)
return .push(viewController, animation: animation)
/* ... */
}
}
}
🛤 深链接
深链接可以用来连接不同的路由。与 .multiple
过渡不同,深链接可以基于先前的过渡(例如在推送或展示一个路由器时)识别路由器,从而实现不同类型路由的链接。请注意,一旦在一个较低级别路由器上触发了一个路由,你将无法访问更高级别的路由器。
class AppCoordinator: NavigationCoordinator<AppRoute> {
/* ... */
override func prepareTransition(for route: AppRoute) -> NavigationTransition {
switch route {
/* ... */
case .deep:
return deepLink(AppRoute.login, AppRoute.home, HomeRoute.news, HomeRoute.dismiss)
}
}
}
🚏 重定向 Router
假设有一个名为 HugeRoute
的路由类型,它包含超过 10 个子路由。为了减少耦合,需要将 HugeRoute
分解为多个路由类型。正如你将要发现的,HugeRoute
中许多路由使用依赖于特定 rootViewController 的过渡,例如 push
、show
、pop
等等。如果通过引入新的路由器/协调器来分解路由不可行,XCoordinator 提供了两个解决方案来解决这种情况:RedirectionRouter
或使用具有相同 rootViewController 的多个协调器(更多信息,请参阅此部分)。
RedirectionRouter
可以用来将新的路由类型映射到通用的 ParentRoute
。一个 RedirectionRouter
与其父路由的 TransitionType
独立。你可以使用 RedirectionRouter.init(viewController:parent:map:)
或通过重写 mapToParentRoute(_:)
来通过子类化创建一个 RedirectionRouter
。
以下代码示例展示了如何初始化和使用 RedirectionRouter
。
class ParentCoordinator: NavigationCoordinator<ParentRoute> {
/* ... */
override func prepareTransition(for route: ParentRoute) -> NavigationTransition {
switch route {
/* ... */
case .child:
let childCoordinator = ChildCoordinator(parent: unownedRouter)
return .push(childCoordinator)
}
}
}
class ChildCoordinator: RedirectionRouter<ParentRoute, ChildRoute> {
init(parent: UnownedRouter<ParentRoute>) {
let viewController = UIViewController()
// this viewController is used when performing transitions with the Subcoordinator directly.
super.init(viewController: viewController, parent: parent, map: nil)
}
/* ... */
override func mapToParentRoute(for route: ChildRoute) -> ParentRoute {
// you can map your ChildRoute enum to ParentRoute cases here that will get triggered on the parent router.
}
}
🚏 使用同一根视图控制器上的多个协调器
使用 XCoordinator 2.0,我们引入了可以使用具有相同根视图控制器的不同协调器的选项。由于你可以在新协调器的初始化器中指定 rootViewController,因此你可以像下面这样指定现有协调器的 rootViewController:
class FirstCoordinator: NavigationCoordinator<FirstRoute> {
/* ... */
override func prepareTransition(for route: FirstRoute) -> NavigationTransition {
switch route {
case .secondCoordinator:
let secondCoordinator = SecondCoordinator(rootViewController: self.rootViewController)
addChild(secondCoordinator)
return .none()
// you could also trigger a specific initial route at this point,
// such as `.trigger(SecondRoute.initial, on: secondCoordinator)`
}
}
}
我们建议不要在同级协调器的初始化器中使用初始路由,而是在 FirstCoordinator
中使用过渡选项。
viewController
属性的情况下推送兄弟协调器),则您的应用可能崩溃。
🚀 RxSwift/Combine扩展
响应式编程非常有用,可以帮助在MVVM架构中保持视图和模型的状态的一致性。您不仅可以依靠任何Router
中可用的trigger
方法的完成处理程序,还可以使用我们的RxSwift扩展。在示例应用程序中,我们使用来自Action框架的Actions来触发某些UI事件上的路由,例如,当点击登录按钮时,触发LoginViewModel
中的LoginRoute.home
。
class LoginViewModelImpl: LoginViewModel, LoginViewModelInput, LoginViewModelOutput {
private let router: UnownedRouter<AppRoute>
private lazy var loginAction = CocoaAction { [unowned self] in
return self.router.rx.trigger(.home)
}
/* ... */
}
除了上述方法之外,响应式的trigger
扩展还可以通过使用flatMap
运算符来按顺序执行不同的转换,如以下
let doneWithBothTransitions =
router.rx.trigger(.home)
.flatMap { [unowned self] in self.router.rx.trigger(.news) }
.map { true }
.startWith(false)
在使用与Combine
扩展一起使用的XCoordinator
时,您可以使用router.publishers.trigger
而不是router.rx.trigger
。
📚 文档 & 示例应用
要获取有关XCoordinator的更多信息,请参阅文档。此外,此存储库是我们使用XCoordinator进行MVVM架构的项目示例。
有关MVC示例应用,请参阅我们关于协调器模式与XCoordinator的一些展示。
👨✈️ 为什么需要协调器
-
责任分离:协调器是唯一一个了解与应用程序流程相关的任何内容的组件。
-
可重用的视图和ViewModel,因为它们不包含任何导航逻辑。
-
组件之间的耦合减少
-
可变的导航:每个协调器只负责一个组件,不需要对其父组件做出任何假设。因此,它可以放在我们想放的地方。
The Coordinator by Soroush Khanlou
⁉️ 为什么选择XCoordinator
- 实际的
导航代码已经写入
并抽象化。 - 清晰的
职责分离
- 协调器:协调一组路由。
- 路由:描述导航路径。
- 转换:描述转换类型和新视图的动画。
重用
协调器、路由和转换的不同组合。- 完全支持
自定义转换/动画
。 - 支持
嵌入子视图
/ 容器视图。 - 通用的
BasicCoordinator
类适合许多用例,因此无需编写自己的协调器。 - 完全支持您的
自定类
,它们符合我们的协调器协议。- 您也可以从以下类型之一开始,以更快地启动:
NavigationCoordinator
、ViewCoordinator
、TabBarCoordinator
等。
- 您也可以从以下类型之一开始,以更快地启动:
- 通用的AnyRouter类型擦除类封装了所有类型的协调器和路由,它们支持相同的一组路由。因此,您可以轻松替换协调器。
- 使用枚举进行路由,这为您提供自动完成和类型安全,以确保您只能执行转换到协调器支持的路径。
🔩 组件
🎢 路由
描述了在流程中可能的导航路径,一个紧密相关的场景集。
👨✈️ 协调器 / 路由器
一个对象,基于触发路由加载视图和创建ViewModel。协调器根据通过路由传输的数据创建和执行转换到这些场景。相比之下,路由器可以被看作是该概念的一种抽象,限于触发路由。通常,路由器用于在ViewModel中抽象特定协调器。
何时使用哪种 Router 抽象
您可以使用Coordinator的unownedRouter
、weakRouter
或strongRouter
属性创建不同的路由抽象。您可以决定以下Coordinator的抽象方式:
- StrongRouter持有一个对原始协调器的强引用。您可以使用此来存储子协调器或指定在
AppDelegate
中使用的特定路由。 - WeakRouter持有一个对原始协调器的弱引用。您可以使用此来存储viewController或viewModel中的协调器。它还可以用来保持对同级或父协调器的引用。
- UnownedRouter持有一个未拥有引用对原始协调器的引用。您可以使用此来在viewController或viewModel中存储协调器。它还可以用来保持对同级或父协调器的引用。
如果您想了解更多关于如何持有引用的不同情况,请参阅这里。
🌗 过渡
过渡描述了一个视图到另一个视图的导航。过渡取决于正在使用的根视图控制器类型。例如:ViewTransition
仅支持每个根视图控制器都支持的简单过渡,而NavigationTransition
则添加了对导航控制器的特定过渡。
可用的过渡类型包括:
- present:在视图层次结构上显示视图控制器 - 如果想从根视图控制器呈现,请使用presentOnRoot
- embed:将视图控制器嵌入到容器视图中
- dismiss:关闭顶部呈现的视图控制器 - 使用dismissToRoot可在根视图控制器上调用关闭
- none:不执行任何操作,可能用于忽略路线或测试目的
- push:将视图控制器推送到导航堆栈中(仅在
NavigationTransition
中) - pop:从导航堆栈中弹出发顶视图控制器(仅在
NavigationTransition
中) - popToRoot:从导航堆栈中删除除根视图控制器外的所有视图控制器(仅在
NavigationTransition
中)
XCoordinator还支持为UITabBarController
、UISplitViewController
和UIPageViewController
根视图控制器提供常用的过渡。
🛠 安装
CocoaPods
要使用CocoaPods将XCoordinator集成到您的Xcode项目中,请将以下代码添加到您的Podfile
中:
pod 'XCoordinator', '~> 2.0'
要使用RxSwift扩展,请将以下代码添加到您的Podfile
中:
pod 'XCoordinator/RxSwift', '~> 2.0'
要使用Combine扩展,请将以下代码添加到您的Podfile
中:
pod 'XCoordinator/Combine', '~> 2.0'
Carthage
要将XCoordinator整合到您的Xcode项目中使用Carthage,请将以下内容添加到您的Cartfile
文件中。
github "quickbirdstudios/XCoordinator" ~> 2.0
然后运行carthage update
命令。
如果是您第一次在项目中使用Carthage,您需要执行一些额外步骤,具体请参考Carthage文档。
Swift包管理器
请参阅这个WWDC演讲了解如何在您的应用中采用Swift包的更多信息。
将https://github.com/quickbirdstudios/XCoordinator.git
指定为XCoordinator
包链接。然后您可以选择三个不同的框架,即XCoordinator
、XCoordinatorRx
和XCoordinatorCombine
。其中XCoordinator
包含主框架,您可以选择XCoordinatorRx
或XCoordinatorCombine
以获取RxSwift
或Combine
扩展。
手动方式
如果您不喜欢使用任何依赖管理器,您可以通过下载源代码并将文件放置在项目目录中来手动将XCoordinator整合到项目中。
👤 作者
此框架是由
有关XCoordinator的更多信息,请参阅我们的博客文章。
❤️ 贡献
如果您需要帮助,如果您找到了一个错误,或者您想讨论功能请求,请打开一个issue。如果您想与开发者和其他用户讨论XCoordinator,请加入我们的Slack工作区。
如果您想对XCoordinator进行修改,请打开一个PR。
📃 许可证
XCoordinator在MIT许可证下发布。有关更多信息,请参阅License.md。