RxFlow 2.13.0

RxFlow 2.13.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最后发布2022年4月
SPM支持 SPM

Thibault Wittemberg 维护。



 
依赖项
RxSwift>= 6.0.0
RxCocoa>= 6.0.0
 


RxFlow 2.13.0

  • Thibault Wittemberg 和 RxSwiftCommunity
RxFlow Logo
GitHub Actions
框架 Carthage Compatible CocoaPods Compatible Swift Package Manager compatible
平台 Platform
许可证 License

关于

RxFlow是一个基于响应式流程协调器模式的iOS应用导航框架。

本README是对整个过程的一个简短叙述,该过程引导我创建了此框架。

你可以在我的博客上找到对整个项目的非常详细的解释。

可以使用 Jazzy 文档在这里看到: 文档

这里还有一个反应式协调器技术演讲,它解释了该框架的目标和动机。该演讲只有俄语。要获取英语字幕,请按 字幕 按钮,查看原始(俄语)字幕,然后选择设置->字幕->翻译->选择你的语言

导航问题

关于iOS应用内的导航,有两种选择可用

  • 使用Apple和Xcode提供的内建机制:故事板和动态路由
  • 在代码中直接实现自定义机制

这两种解决方案的缺点

  • 内建机制:导航相对静态,故事板巨大。导航代码污染了UIViewControllers
  • 自定义机制:代码的设置可能比较困难,并且复杂程度取决于选择的设计模式(路由器,协调器)

RxFlow致力于

  • 将故事板分割成原子单元以促进UIViewControllers的协作和重用
  • 根据导航上下文以不同方式呈现UIViewController
  • 简化依赖注入的实现
  • 从UIViewControllers中移除所有导航机制
  • 推广响应式编程
  • 以声明式的方式表达导航,同时解决大多数导航场景
  • 方便将应用程序分割成逻辑导航块

安装

Carthage

在你的Cartfile

github "RxSwiftCommunity/RxFlow"

CocoaPods

在你的Podfile

pod 'RxFlow'

Swift包管理器

在你的Package.swift

let package = Package(
  name: "Example",
  dependencies: [
    .package(url: "https://github.com/RxSwiftCommunity/RxFlow.git", from: "2.10.0")
  ],
  targets: [
    .target(name: "Example", dependencies: ["RxFlow"])
  ]
)

关键原则

协调器模式是组织应用程序内导航的一种很好的方式。它允许

  • 从UIViewControllers中删除导航代码。
  • 在不同导航上下文中重用UIViewControllers。
  • 简化依赖注入的使用。

要了解更多相关信息,我建议您阅读这篇文章:(Coordinator Redux)。

尽管如此,协调器模式也有一些缺点

  • 协调机制必须每次启动应用程序时都编写。
  • 与协调器堆栈通信可能导致大量样板代码。

RxFlow 是协调器模式的一个响应式实现。它具有这种架构的所有优秀功能,但也带来了一些改进

  • 它使得在 Flows 中的导航更加声明式。
  • 它提供了一个内置的 FlowCoordinator,用于处理 Flows 之间的导航。
  • 它使用响应式编程来触发针对 FlowCoordinators 的导航操作。

为了理解 RxFlow,您需要熟悉 6 个术语

  • Flow:每个 Flow 定义了应用程序中的一个导航区域。这是您声明导航操作(例如展示 UIViewController 或另一个 Flow)的地方。
  • Step:一个 Step 是一种表达可以导致导航的状态的方式。组合 FlowsSteps 描述了所有可能的导航操作。一个 Step 甚至可以嵌入内嵌值(例如 Ids,URLs,...),这些值将传播到在 Flows 中声明的屏幕。
  • Stepper:一个 Stepper 可以是 Flows 内部可以发出 Steps 的任何东西。
  • Presentable:它是可以展示的某物的一个抽象(基本上,UIViewControllerFlow 都是 Presentable)。
  • FlowContributor:它是一个简单的数据结构,告诉 FlowCoordinatorFlow 中可以发出新 Steps 的下一个内容。
  • FlowCoordinator:一旦开发人员定义了代表导航可能性的合适的 FlowsSteps 的组合,FlowCoordinator 的任务就是混合这些组合来处理应用程序的所有导航。FlowCoordinatorsRxFlow 提供,您不需要实现它们。

如何使用 RxFlow

代码示例

如何声明 Steps

Steps 是表示最终导致导航意图的小状态片段,在枚举中声明它们非常方便。

enum DemoStep: Step {
    // Login
    case loginIsRequired
    case userIsLoggedIn

    // Onboarding
    case onboardingIsRequired
    case onboardingIsComplete

    // Home
    case dashboardIsRequired

    // Movies
    case moviesAreRequired
    case movieIsPicked (withId: Int)
    case castIsPicked (withId: Int)

    // Settings
    case settingsAreRequired
    case settingsAreComplete
}

思路是要尽可能保持 步骤 导航独立。例如,调用一个 步骤 showMovieDetail(withId: Int) 可能是不好的想法,因为这会紧密地将选择电影与显示电影详情屏幕的结果关联在一起。决定导航去往何处的权力并不属于 步骤 的发出者,这个决定属于 流程

如何声明一个 流程

以下 流程 被用作导航栈。你所需要做的只是

  • 在根 可展示对象 上声明你的导航。
  • 实现 navigate(to:) 函数,将 步骤 转换为导航操作。

流程 可用于实现初始化 ViewControllers 时的依赖注入。

navigate(to:) 函数返回 FlowContributors。这就是如何产生后续的导航操作。

例如,值:.one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: viewController.viewModel)) 代表

  • viewController 是一个 可展示对象,其生命周期将影响相关 步进器 发射 步骤 的方式。例如,如果一个 步进器 在其相关 可展示对象 暂时隐藏时发出一个 步骤,则该 步骤 不会被处理。
  • viewController.viewModel 是一个 步进器,它将通过根据其相关 可展示对象 的生命周期发射 步骤 来对该 流程 的导航做出贡献。
class WatchedFlow: Flow {
    var root: Presentable {
        return self.rootViewController
    }

    private let rootViewController = UINavigationController()
    private let services: AppServices

    init(withServices services: AppServices) {
        self.services = services
    }

    func navigate(to step: Step) -> FlowContributors {

        guard let step = step as? DemoStep else { return .none }

        switch step {

        case .moviesAreRequired:
            return navigateToMovieListScreen()
        case .movieIsPicked(let movieId):
            return navigateToMovieDetailScreen(with: movieId)
        case .castIsPicked(let castId):
            return navigateToCastDetailScreen(with: castId)
        default:
            return .none
        }
    }

    private func navigateToMovieListScreen() -> FlowContributors {
        let viewController = WatchedViewController.instantiate(withViewModel: WatchedViewModel(),
                                                               andServices: self.services)
        viewController.title = "Watched"

        self.rootViewController.pushViewController(viewController, animated: true)
        return .one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: viewController.viewModel))
    }

    private func navigateToMovieDetailScreen (with movieId: Int) -> FlowContributors {
        let viewController = MovieDetailViewController.instantiate(withViewModel: MovieDetailViewModel(withMovieId: movieId),
                                                                   andServices: self.services)
        viewController.title = viewController.viewModel.title
        self.rootViewController.pushViewController(viewController, animated: true)
        return .one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: viewController.viewModel))
    }

    private func navigateToCastDetailScreen (with castId: Int) -> FlowContributors {
        let viewController = CastDetailViewController.instantiate(withViewModel: CastDetailViewModel(withCastId: castId),
                                                                  andServices: self.services)
        viewController.title = viewController.viewModel.name
        self.rootViewController.pushViewController(viewController, animated: true)
        return .none
    }
}

如何处理深度链接

从 AppDelegate 中,你可以访问 FlowCoordinator 并在收到通知时调用 navigate(to:) 函数。

函数传递的步骤随后将被传递给所有现有的流程,这样你可以适应导航。

func userNotificationCenter(_ center: UNUserNotificationCenter,
                            didReceive response: UNNotificationResponse,
                            withCompletionHandler completionHandler: @escaping () -> Void) {
    // example of how DeepLink can be handled
    self.coordinator.navigate(to: DemoStep.movieIsPicked(withId: 23452))
}

如何在步骤触发导航之前对其进行适配?

流程有一个 adapt(step:) -> Single<Step> 函数,默认返回它作为参数给出的步骤。

此函数在 Flow Coordinator 调用 navigate(to:) 函数之前被调用。这是实现一些逻辑的好地方,例如,可以在此禁用某些步骤的导航。一个常见的用例是在应用程序中处理导航权限。

假设我们有一个 PermissionManager

func adapt(step: Step) -> Single<Step> {
    switch step {
    case DemoStep.aboutIsRequired:
        return PermissionManager.isAuthorized() ? .just(step) : .just(DemoStep.unauthorized)     
    default:
        return .just(step)         
    }
}

...

later in the navigate(to:) function, the .unauthorized step could trigger an AlertViewController

为什么返回单个元素而不是直接返回步进?因为一些过滤过程可能是异步的,并且需要用户操作来执行(例如,基于设备的认证层(如TouchID或FaceID)的过滤)。

为了提高关注点的分离,可以使用注入代理的方式来定义一个流(Flow),该代理的目的就是为了处理在adapt(step:)函数中的适配。这个代理可以在多个流中被重复使用,以确保适配的一致性。

如何声明一个步进器(Stepper)

从理论上讲,作为协议,步进器(Stepper)可以是任何东西(例如,一个UIViewController),但一个好的实践是将这种行为隔离在ViewModel或其他类似的示例中。

RxFlow提供了一个预定义的OneStepper类。例如,在创建新的Flow时,可以使用它来表示将指导导航的第一个步进(Step)

以下步进器每次调用pick(movieId:)函数时都会发出DemoStep.moviePicked(withMovieId:)。然后,WatchedFlow将调用函数navigateToMovieDetailScreen (with movieId: Int)

class WatchedViewModel: Stepper {

    let movies: [MovieViewModel]
    let steps = PublishRelay<Step>()

    init(with service: MoviesService) {
        // we can do some data refactoring in order to display things exactly the way we want (this is the aim of a ViewModel)
        self.movies = service.watchedMovies().map({ (movie) -> MovieViewModel in
            return MovieViewModel(id: movie.id, title: movie.title, image: movie.image)
        })
    }

    // when a movie is picked, a new Step is emitted.
    // That will trigger a navigation action within the WatchedFlow
    public func pick (movieId: Int) {
        self.steps.accept(DemoStep.movieIsPicked(withId: movieId))
    }

}

是否可以协调多个流(Flow)?

当然可以,这是协调器(Coordinator)的目的。在一个流中,我们可以展示UIViewControllers以及新的Flow。函数Flows.whenReady()允许在新的Flow准备好显示时被触发,并返回它的根Presentable

例如,从WishlistFlow,我们可以在一个弹出窗口中启动SettingsFlow。

private func navigateToSettings() -> FlowContributors {
	let settingsStepper = SettingsStepper()
	let settingsFlow = SettingsFlow(withServices: self.services, andStepper: settingsStepper)

    Flows.use(settingsFlow, when: .ready) { [unowned self] root in
        self.rootViewController.present(root, animated: true)
    }
    
    return .one(flowContributor: .contribute(withNextPresentable: settingsFlow, withNextStepper: settingsStepper))
    }

Flows.use(when:)方法接收一个作为第二个参数的ExecuteStrategy。它有两个可能的值

  • .created: 完成块将立即执行
  • .ready: 完成块将在子流(例如示例中的SettingsFlow)发出第一个步骤后执行

对于更复杂的情况,请参阅DashboardFlow.swiftSettingsFlow.swift文件,其中我们处理了UITabBarController和UISplitViewController。

如何引导RxFlow过程

协调过程相当直接,发生在AppDelegate中。

class AppDelegate: UIResponder, UIApplicationDelegate {

    let disposeBag = DisposeBag()
    var window: UIWindow?
    var coordinator = FlowCoordinator()
    let appServices = AppServices()

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        guard let window = self.window else { return false }

        // listening for the coordination mechanism is not mandatory, but can be useful
        coordinator.rx.didNavigate.subscribe(onNext: { (flow, step) in
            print ("did navigate to flow=\(flow) and step=\(step)")
        }).disposed(by: self.disposeBag)

        let appFlow = AppFlow(withWindow: window, andServices: self.appServices)
        self.coordinator.coordinate(flow: self.appFlow, with: AppStepper(withServices: self.appServices))

        return true
    }
}

作为额外功能,FlowCoordinator 提供了 Rx 扩展,允许您跟踪导航操作(FlowCoordinator.rx.willNavigateFlowCoordinator.rx.didNavigate)。

演示应用程序

提供了演示应用程序以说明核心机制。几乎涵盖了所有类型的导航。应用程序包括:

  • 表示主要导航的 AppFlow。这个流程将根据用户的 "onboarding 状态" 处理 OnboardingFlow 和 DashboardFlow。
  • 表示 UINavigationController 中的 2 步引导流程的 OnBoardingFlow。它只会在首次使用应用程序时显示。
  • 处理 WishlistFlow 和 WatchedFlow 标签栏的 DashboardFlow。
  • 表示您想要观看的电影导航栈的 WishlistFlow。
  • 表示您已经看过的电影的导航栈的 WatchedFlow。
  • 表示用户偏好的 SettingsFlow,以主/详细信息表示形式。

Demo Application

工具和依赖

RxFlow 依赖于

  • SwiftLint 进行静态代码分析(GitHub SwiftLint
  • RxSwift 将步骤暴露为可观察对象,让 Coordinator 能够做出反应(GitHub Rx Swift
  • 在 Demo App 中可重复使用,以便将 storyboard 切片到原子 ViewController 中(GitHub Reusable