Build Status CocoaPods Compatible Carthage Compatible Documentation Platform License

⚠️我们最近发布了 XCoordinator 2.0。在迁移之前,请务必阅读此部分。 通常,请将所有 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)
    }
}

🏗 使用 XCoordinator 组织应用程序的结构

通常,应用程序的结构由嵌套的协调器和视图控制器定义。 每当您的应用程序更改为不同的流程时,您可以转换(即 pushpresentpopdismiss)到不同的协调器。 在一个流程中,我们在 viewControllers 之间进行转换。

示例:在 UserListCoordinator.prepareTransition(for:) 中,每当触发 UserListRoute.user 路由时,我们从 UserListRoute 更改为 UserRoute。 通过在 UserListRoute.logout 中关闭一个 viewController,我们还会切换回先前的流程 - 在这种情况下是 HomeRoute

为了实现这种行为,每个 Coordinator 都有自己的 rootViewController。 在 NavigationCoordinator 的情况下,这将是一个 UINavigationController,在 TabBarCoordinator 的情况下,这将是一个 UITabBarController,等等。 当转换到 Coordinator/Router 时,此 rootViewController 用作目标视图控制器。

🏁 从应用程序启动使用 XCoordinator

要从应用程序启动时使用协调器,请确保在 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 对象,例如 presentdismisspushpop。 不指定动画 (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)
        }
    }
}

⚠️XCoordinator 不会在编译时检查是否可以执行深度链接。 相反,它使用 assertionFailures 来告知运行时不正确的链接,因为它无法为给定的路由找到合适的路由器。 在更改应用程序的结构时请记住这一点。

🚏 RedirectionRouter

假设存在一种名为 HugeRoute 的路由类型,其中包含超过 10 个路由。 为了减少耦合,需要将 HugeRoute 拆分为多个路由类型。 您会发现,HugeRoute 中的许多路由都使用依赖于特定 rootViewController 的转换,例如 pushshowpop 等。 如果通过引入新的路由器/协调器来拆分路由不是一个选项,那么 XCoordinator 有两种解决方案可以解决这种情况:RedirectionRouter 或使用具有相同 rootViewController 的多个协调器(有关更多信息,请参阅此部分)。

可以使用 RedirectionRouter 将新的路由类型映射到泛化的 ParentRouteRedirectionRouter 独立于其父路由器的 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.
    }
}

🚏使用具有相同 rootViewController 的多个协调器

在 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 属性),您的应用程序很可能会崩溃。

🚀 RxSwift/Combine 扩展

反应式编程对于在 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)

XCoordinatorCombine 扩展一起使用时,您可以使用 router.publishers.trigger 而不是 router.rx.trigger

📚 文档 & 示例应用程序

要获取有关 XCoordinator 的更多信息,请查看文档。 此外,此存储库用作使用 MVVM 架构与 XCoordinator 的示例项目。

有关 MVC 示例应用程序,请查看我们关于协调器模式和 XCoordinator 的一些演示文稿

👨‍💻✈️为什么要使用协调器

协调器作者:Soroush Khanlou

⁉️为什么要使用 XCoordinator

🔩 组件

🎢 路由

描述了流程中可能的导航路径,流程是一组紧密相关的场景。

👨‍💻✈️协调器/路由器

一个基于触发的路由加载视图并创建 viewModels 的对象。 协调器基于通过路由传输的数据创建并执行到这些场景的过渡。 与协调器相比,路由器可以被看作是从该概念抽象出来的,仅限于触发路由。 通常,路由器用于抽象 ViewModel 中的特定协调器。

何时使用哪种路由器抽象

您可以使用 CoordinatorunownedRouterweakRouterstrongRouter 属性创建不同的路由器抽象。 您可以在协调器的以下路由器抽象之间进行选择

如果您想了解更多关于如何保持引用的差异,请查看此处

🌗 过渡

转场描述了从一个视图导航到另一个视图的过程。可用的转场类型取决于正在使用的根视图控制器的类型。例如:ViewTransition 仅支持每个根视图控制器都支持的基本转场,而 NavigationTransition 则添加了导航控制器特定的转场。

可用的转场类型包括:

XCoordinator 还支持 UITabBarControllerUISplitViewControllerUIPageViewController 根视图控制器的常用转场。

🛠 安装

CocoaPods

要使用 CocoaPods 将 XCoordinator 集成到您的 Xcode 项目中,请将以下内容添加到您的 Podfile

pod 'XCoordinator', '~> 2.0'

要使用 RxSwift 扩展,请将以下内容添加到您的 Podfile

pod 'XCoordinator/RxSwift', '~> 2.0'

要使用 Combine 扩展,请将以下内容添加到您的 Podfile

pod 'XCoordinator/Combine', '~> 2.0'

Carthage

要使用 Carthage 将 XCoordinator 集成到您的 Xcode 项目中,请将以下内容添加到您的 Cartfile

github "quickbirdstudios/XCoordinator" ~> 2.0

然后运行 carthage update

如果这是您第一次在项目中使用 Carthage,您需要执行一些额外的步骤,如 Carthage 中所述。

Swift Package Manager

有关如何在您的应用程序中采用 Swift 包的更多信息,请参阅此 WWDC 演示文稿

https://github.com/quickbirdstudios/XCoordinator.git 指定为 XCoordinator 包链接。 然后,您可以在三个不同的框架之间进行选择,即 XCoordinatorXCoordinatorRxXCoordinatorCombine。 虽然 XCoordinator 包含主框架,但您可以选择 XCoordinatorRxXCoordinatorCombine 来获取 RxSwiftCombine 扩展。

手动

如果您不想使用任何依赖管理器,您可以通过下载源代码并将文件放置在您的项目目录中,手动将 XCoordinator 集成到您的项目中。

👤 作者

此框架由 ❤️ QuickBird Studios 创建。

要获取有关 XCoordinator 的更多信息,请查看 我们的博客文章

❤️ 贡献

如果您需要帮助、发现错误或想要讨论功能请求,请打开一个 issue。 如果您想与开发人员和其他用户一起聊天关于 XCoordinator 的事情,请加入我们的 Slack 工作区

如果您想更改 XCoordinator,请打开一个 PR。

📃 许可证

XCoordinator 在 MIT 许可证下发布。 有关更多信息,请参阅 License.md