XCoordinator 2.2.1

XCoordinator 2.2.1

Paul KraftStefan Kofler维护。



  • Stefan Kofler 和 Paul Kraft

Build Status CocoaPods Compatible Carthage Compatible Documentation Platform License

⚠️我们最近发布了XCoordinator 2.0。在迁移之前,请务必阅读本节。一般来说,请将所有的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 对象,例如 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 在运行时通知您链式调用不正确,当它找不到给定路由的适当路由器时。请记住,在更改应用程序结构时要注意这一点。

🚏重定向 Router

假设有一个名为 HugeRoute 的路由类型,它包含超过 10 个子路由。为了减少耦合,需要将 HugeRoute 分解为多个路由类型。正如你将要发现的,HugeRoute 中许多路由使用依赖于特定 rootViewController 的过渡,例如 pushshowpop 等等。如果通过引入新的路由器/协调器来分解路由不可行,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类适合许多用例,因此无需编写自己的协调器。
  • 完全支持您的自定类,它们符合我们的协调器协议。
    • 您也可以从以下类型之一开始,以更快地启动:NavigationCoordinatorViewCoordinatorTabBarCoordinator等。
  • 通用的AnyRouter类型擦除类封装了所有类型的协调器和路由,它们支持相同的一组路由。因此,您可以轻松替换协调器。
  • 使用枚举进行路由,这为您提供自动完成和类型安全,以确保您只能执行转换到协调器支持的路径。

🔩组件

🎢路由

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

👨‍✈️协调器 / 路由器

一个对象,基于触发路由加载视图和创建ViewModel。协调器根据通过路由传输的数据创建和执行转换到这些场景。相比之下,路由器可以被看作是该概念的一种抽象,限于触发路由。通常,路由器用于在ViewModel中抽象特定协调器。

何时使用哪种 Router 抽象

您可以使用Coordinator的unownedRouterweakRouterstrongRouter属性创建不同的路由抽象。您可以决定以下Coordinator的抽象方式:

  • StrongRouter持有一个对原始协调器的强引用。您可以使用此来存储子协调器或指定在AppDelegate中使用的特定路由。
  • WeakRouter持有一个对原始协调器的弱引用。您可以使用此来存储viewController或viewModel中的协调器。它还可以用来保持对同级或父协调器的引用。
  • UnownedRouter持有一个未拥有引用对原始协调器的引用。您可以使用此来在viewController或viewModel中存储协调器。它还可以用来保持对同级或父协调器的引用。

如果您想了解更多关于如何持有引用的不同情况,请参阅这里

🌗过渡

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

可用的过渡类型包括:

  • present:在视图层次结构上显示视图控制器 - 如果想从根视图控制器呈现,请使用presentOnRoot
  • embed:将视图控制器嵌入到容器视图中
  • dismiss:关闭顶部呈现的视图控制器 - 使用dismissToRoot可在根视图控制器上调用关闭
  • none:不执行任何操作,可能用于忽略路线或测试目的
  • push:将视图控制器推送到导航堆栈中(仅在NavigationTransition中)
  • pop:从导航堆栈中弹出发顶视图控制器(仅在NavigationTransition中)
  • popToRoot:从导航堆栈中删除除根视图控制器外的所有视图控制器(仅在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

要将XCoordinator整合到您的Xcode项目中使用Carthage,请将以下内容添加到您的Cartfile文件中。

github "quickbirdstudios/XCoordinator" ~> 2.0

然后运行carthage update命令。

如果是您第一次在项目中使用Carthage,您需要执行一些额外步骤,具体请参考Carthage文档

Swift包管理器

请参阅这个WWDC演讲了解如何在您的应用中采用Swift包的更多信息。

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

手动方式

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

👤作者

此框架是由❤️QuickBird Studios制作的。

有关XCoordinator的更多信息,请参阅我们的博客文章

❤️贡献

如果您需要帮助,如果您找到了一个错误,或者您想讨论功能请求,请打开一个issue。如果您想与开发者和其他用户讨论XCoordinator,请加入我们的Slack工作区

如果您想对XCoordinator进行修改,请打开一个PR。

📃许可证

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