GitHub Actions | |
框架 | |
平台 | |
许可证 |
关于
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 是一种表达可以导致导航的状态的方式。组合 Flows 和 Steps 描述了所有可能的导航操作。一个 Step 甚至可以嵌入内嵌值(例如 Ids,URLs,...),这些值将传播到在 Flows 中声明的屏幕。
- Stepper:一个 Stepper 可以是 Flows 内部可以发出 Steps 的任何东西。
- Presentable:它是可以展示的某物的一个抽象(基本上,UIViewController 和 Flow 都是 Presentable)。
- FlowContributor:它是一个简单的数据结构,告诉 FlowCoordinator 在 Flow 中可以发出新 Steps 的下一个内容。
- FlowCoordinator:一旦开发人员定义了代表导航可能性的合适的 Flows 和 Steps 的组合,FlowCoordinator 的任务就是混合这些组合来处理应用程序的所有导航。FlowCoordinators 由 RxFlow 提供,您不需要实现它们。
如何使用 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.swift
和SettingsFlow.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.willNavigate 和 FlowCoordinator.rx.didNavigate)。
演示应用程序
提供了演示应用程序以说明核心机制。几乎涵盖了所有类型的导航。应用程序包括:
- 表示主要导航的 AppFlow。这个流程将根据用户的 "onboarding 状态" 处理 OnboardingFlow 和 DashboardFlow。
- 表示 UINavigationController 中的 2 步引导流程的 OnBoardingFlow。它只会在首次使用应用程序时显示。
- 处理 WishlistFlow 和 WatchedFlow 标签栏的 DashboardFlow。
- 表示您想要观看的电影导航栈的 WishlistFlow。
- 表示您已经看过的电影的导航栈的 WatchedFlow。
- 表示用户偏好的 SettingsFlow,以主/详细信息表示形式。
工具和依赖
RxFlow 依赖于
- SwiftLint 进行静态代码分析(GitHub SwiftLint)
- RxSwift 将步骤暴露为可观察对象,让 Coordinator 能够做出反应(GitHub Rx Swift)
- 在 Demo App 中可重复使用,以便将 storyboard 切片到原子 ViewController 中(GitHub Reusable)