AnyRouter
替换为 UnownedRouter
(在 viewControllers、viewModels 或父协调器的引用中) 或 StrongRouter
(在您的 AppDelegate
中或子协调器的引用中)。 除此之外,现在根视图控制器被注入到初始化器中,而不是在 Coordinator.generateRootViewController
方法中创建。
“一个应用程序如何从一个视图控制器转换到另一个视图控制器?”。 这是关于 iOS 开发的常见且令人困惑的问题。 答案有很多,因为每种架构都有不同的实现变体。 一些从视图控制器的实现内部进行,而另一些使用路由器/协调器,一个连接视图模型的对象。
为了更好地回答这个问题,我们正在构建 XCoordinator,一个基于 Coordinator 模式的导航框架。 它对于实现 MVVM-C(Model-View-ViewModel-Coordinator)特别有用。
创建一个枚举,其中包含特定流程的所有导航路径,即一组紧密连接的场景。(何时创建 Route/Coordinator
由您决定。 作为 我们的经验法则,每当需要一个新的根视图控制器时,例如一个新的 navigation controller
或一个 tab bar controller
,就创建一个新的 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)
}
}
通常,应用程序的结构由嵌套的协调器和视图控制器定义。 每当您的应用程序更改为不同的流程时,您可以转换(即 push
、present
、pop
、dismiss
)到不同的协调器。 在一个流程中,我们在 viewControllers 之间进行转换。
示例:在 UserListCoordinator.prepareTransition(for:)
中,每当触发 UserListRoute.user
路由时,我们从 UserListRoute
更改为 UserRoute
。 通过在 UserListRoute.logout
中关闭一个 viewController,我们还会切换回先前的流程 - 在这种情况下是 HomeRoute
。
为了实现这种行为,每个 Coordinator 都有自己的 rootViewController
。 在 NavigationCoordinator
的情况下,这将是一个 UINavigationController
,在 TabBarCoordinator
的情况下,这将是一个 UITabBarController
,等等。 当转换到 Coordinator/Router 时,此 rootViewController
用作目标视图控制器。
要从应用程序启动时使用协调器,请确保在 AppDelegate.swift
中以编程方式创建应用程序的 window
(不要忘记从 Info.plist
中删除 Main Storyboard file base name
)。 然后,将协调器设置为 window
视图层次结构的根,位于 AppDelegate.didFinishLaunching
中。 确保保留对应用程序初始协调器或 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)
}
}
}
假设存在一种名为 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,因此您可以指定现有协调器的 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
属性),您的应用程序很可能会崩溃。
反应式编程对于在 MVVM 架构中保持视图和模型的状态一致非常有用。 除了依赖于任何 Router
中可用的 trigger
方法的完成处理程序之外,您还可以使用我们的 RxSwift 扩展。 在示例应用程序中,我们使用 Actions(来自 Action 框架)来触发某些 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)
将 XCoordinator
与 Combine
扩展一起使用时,您可以使用 router.publishers.trigger
而不是 router.rx.trigger
。
要获取有关 XCoordinator 的更多信息,请查看文档。 此外,此存储库用作使用 MVVM 架构与 XCoordinator 的示例项目。
有关 MVC 示例应用程序,请查看我们关于协调器模式和 XCoordinator 的一些演示文稿。
职责分离,协调器是唯一了解与应用程序流程相关的组件。
可重用的视图和 ViewModel,因为它们不包含任何导航逻辑。
组件之间的耦合更少
可更改的导航:每个协调器仅负责一个组件,不需要对其父组件进行任何假设。 因此,我们可以将其放置在我们想要的任何地方。
协调器作者:Soroush Khanlou
BasicCoordinator
类适用于许多用例,因此减少了编写自己的协调器的需求。NavigationCoordinator
、ViewCoordinator
、TabBarCoordinator
等。描述了流程中可能的导航路径,流程是一组紧密相关的场景。
一个基于触发的路由加载视图并创建 viewModels 的对象。 协调器基于通过路由传输的数据创建并执行到这些场景的过渡。 与协调器相比,路由器可以被看作是从该概念抽象出来的,仅限于触发路由。 通常,路由器用于抽象 ViewModel 中的特定协调器。
您可以使用 Coordinator
的 unownedRouter
、weakRouter
或 strongRouter
属性创建不同的路由器抽象。 您可以在协调器的以下路由器抽象之间进行选择
AppDelegate
中指定某个路由器。如果您想了解更多关于如何保持引用的差异,请查看此处。
转场描述了从一个视图导航到另一个视图的过程。可用的转场类型取决于正在使用的根视图控制器的类型。例如:ViewTransition
仅支持每个根视图控制器都支持的基本转场,而 NavigationTransition
则添加了导航控制器特定的转场。
可用的转场类型包括:
NavigationTransition
中)NavigationTransition
中)NavigationTransition
中)XCoordinator 还支持 UITabBarController
、UISplitViewController
和 UIPageViewController
根视图控制器的常用转场。
要使用 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 项目中,请将以下内容添加到您的 Cartfile
中
github "quickbirdstudios/XCoordinator" ~> 2.0
然后运行 carthage update
。
如果这是您第一次在项目中使用 Carthage,您需要执行一些额外的步骤,如 Carthage 中所述。
有关如何在您的应用程序中采用 Swift 包的更多信息,请参阅此 WWDC 演示文稿。
将 https://github.com/quickbirdstudios/XCoordinator.git
指定为 XCoordinator
包链接。 然后,您可以在三个不同的框架之间进行选择,即 XCoordinator
、XCoordinatorRx
和 XCoordinatorCombine
。 虽然 XCoordinator
包含主框架,但您可以选择 XCoordinatorRx
或 XCoordinatorCombine
来获取 RxSwift
或 Combine
扩展。
如果您不想使用任何依赖管理器,您可以通过下载源代码并将文件放置在您的项目目录中,手动将 XCoordinator 集成到您的项目中。
此框架由 ❤️ QuickBird Studios 创建。
要获取有关 XCoordinator 的更多信息,请查看 我们的博客文章。
如果您需要帮助、发现错误或想要讨论功能请求,请打开一个 issue。 如果您想与开发人员和其他用户一起聊天关于 XCoordinator 的事情,请加入我们的 Slack 工作区。
如果您想更改 XCoordinator,请打开一个 PR。
XCoordinator 在 MIT 许可证下发布。 有关更多信息,请参阅 License.md。