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

关于

RxFlow 是一个基于 响应式流程协调器模式 (Reactive Flow Coordinator pattern) 的 iOS 应用程序导航框架。

本 README 简述了引导我开发此框架的整个概念过程。

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

Jazzy 文档也可以在此处查看:文档

这里还有一个 响应式协调器技术讲座,它解释了该框架的目标和动机。 仅提供俄语版本。 要获得英文字幕,您应该按字幕按钮以查看原始(俄语)字幕,然后选择“设置”->“字幕”->“翻译”->选择您的语言

导航相关问题

关于 iOS 应用程序中的导航,有两种选择

这两种解决方案的缺点

RxFlow 的目标

安装

Carthage

在你的 Cartfile 中

github "RxSwiftCommunity/RxFlow"

CocoaPods

在你的 Podfile 中

pod 'RxFlow'

Swift Package Manager

在你的 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"])
  ]
)

核心原则

Coordinator 模式是组织应用程序中导航的绝佳方法。 它允许

要了解更多信息,我建议你查看这篇文章:(Coordinator Redux)。

然而,Coordinator 模式也可能存在一些缺点

RxFlow 是 Coordinator 模式的响应式实现。 它具有此架构的所有强大功能,但带来了一些改进

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

如何使用 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
}

我们的想法是尽可能保持 Steps navigation independent。 例如,调用 Step showMovieDetail(withId: Int) 可能不是一个好主意,因为它将选择电影的事实与显示电影详细信息屏幕的后果紧密结合在一起。 决定导航到哪里不是 Step 的发射器决定的,这个决定属于 Flow

如何声明 Flow

以下 Flow 用作导航堆栈。 你所要做的就是

Flows 可用于在实例化 ViewControllers 时实现依赖注入。

navigate(to:) 函数返回一个 FlowContributors。 这就是生成下一个导航操作的方式。

例如,值:.one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: 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:) 函数。

传递给该函数的 step 随后将被传递给所有现有的 Flows,因此您可以调整导航。

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))
}

如何在 Step 触发导航之前调整它?

Flow 有一个 adapt(step:) -> Single<Step> 函数,默认情况下返回作为参数给定的 step。

FlowCoordinator 在 navigate(to:) 函数之前调用此函数。 这是实现一些逻辑的绝佳位置,例如,该逻辑可以禁止 step 触发导航。 一个常见的用例是处理应用程序中的导航权限。

假设我们有一个 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

为什么要返回 Single 而不是直接返回 Step? 因为一些过滤过程可能是异步的,需要用户操作才能执行(例如,基于具有 TouchID 或 FaceID 的设备的身份验证层进行过滤)

为了提高关注点分离,可以将委托注入到 Flow 中,其目的是处理 adapt(step:) 函数中的改编。 委托最终可以在多个流中重用,以确保改编的一致性。

如何声明 Stepper

理论上,Stepper 作为一个协议,可以是任何东西(例如 UIViewController),但一个好的做法是将该行为隔离在 ViewModel 或类似的东西中。

RxFlow 附带一个预定义的 OneStepper 类。 例如,可以在创建新 Flow 时使用它来表达将驱动导航的第一个 Step

以下 Stepper 将在每次调用函数 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))
    }

}

是否可以协调多个 Flows?

当然,这是 Coordinator 的目标。 在 Flow 中,我们可以呈现 UIViewControllers 以及新的 Flows。 函数 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 作为第二个参数。 它有两个可能的值

对于更复杂的情况,请参阅 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)。

演示应用程序

提供了一个演示应用程序来说明核心机制。 几乎解决了每种导航方式。 该应用程序包括


Demo Application

工具和依赖

RxFlow 依赖于