RxFlow
RxFlow is a navigation framework for iOS applications based on a Reactive Flow Coordinator pattern
Install / Use
/learn @RxSwiftCommunity/RxFlowREADME
| <img alt="RxFlow Logo" src="https://raw.githubusercontent.com/RxSwiftCommunity/RxFlow/develop/Resources/RxFlow_logo.png" width="250"/> | <ul align="left"><li><a href="#about">About</a><li><a href="#navigation-concerns">Navigation concerns</a><li><a href="#rxflow-aims-to">RxFlow aims to</a><li><a href="#installation">Installation</a><li><a href="#the-key-principles">The key principles</a><li><a href="#how-to-use-rxflow">How to use RxFlow</a><li><a href="#tools-and-dependencies">Tools and dependencies</a></ul> |
| -------------- | -------------- |
| GitHub Actions | |
| Frameworks |
|
| Platform |
|
| Licence |
|
About
RxFlow is a navigation framework for iOS applications based on a Reactive Flow Coordinator pattern.
This README is a short story of the whole conception process that led me to this framework.
You will find a very detail explanation of the whole project on my blog:
The Jazzy documentation can be seen here as well: Documentation
Also here is a Reactive coordinators tech talk which explain the goals and motivation of that framework. Available only in Russian. To get English subtitles you should press the subtitles button to see original (Russian) subtitles and then select Settings->Subtitles->Translate->choose_your_language
Navigation concerns
Regarding navigation within an iOS application, two choices are available:
- Use the builtin mechanism provided by Apple and Xcode: storyboards and segues
- Implement a custom mechanism directly in the code
The disadvantage of these two solutions:
- Builtin mechanism: navigation is relatively static and the storyboards are massive. The navigation code pollutes the UIViewControllers
- Custom mechanism: code can be difficult to set up and can be complex depending on the chosen design pattern (Router, Coordinator)
RxFlow aims to
- Promote the cutting of storyboards into atomic units to enable collaboration and reusability of UIViewControllers
- Allow the presentation of a UIViewController in different ways according to the navigation context
- Ease the implementation of dependency injection
- Remove every navigation mechanism from UIViewControllers
- Promote reactive programming
- Express the navigation in a declarative way while addressing the majority of the navigation cases
- Facilitate the cutting of an application into logical blocks of navigation
Installation
Carthage
In your Cartfile:
github "RxSwiftCommunity/RxFlow"
CocoaPods
In your Podfile:
pod 'RxFlow'
Swift Package Manager
In your 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"])
]
)
The key principles
The Coordinator pattern is a great way to organize the navigation within your application. It allows to:
- Remove the navigation code from UIViewControllers.
- Reuse UIViewControllers in different navigation contexts.
- Ease the use of dependency injection.
To learn more about it, I suggest you take a look at this article: (Coordinator Redux).
Nevertheless, the Coordinator pattern can have some drawbacks:
- The coordination mechanism has to be written each time you bootstrap an application.
- Communicating with the Coordinators stack can lead to a lot of boilerplate code.
RxFlow is a reactive implementation of the Coordinator pattern. It has all the great features of this architecture, but brings some improvements:
- It makes the navigation more declarative within Flows.
- It provides a built-in FlowCoordinator that handles the navigation between Flows.
- It uses reactive programming to trigger navigation actions towards the FlowCoordinators.
There are 6 terms you have to be familiar with to understand RxFlow:
- Flow: each Flow defines a navigation area in your application. This is the place where you declare the navigation actions (such as presenting a UIViewController or another Flow).
- Step: a Step is a way to express a state that can lead to a navigation. Combinations of Flows and Steps describe all the possible navigation actions. A Step can even embed inner values (such as Ids, URLs, ...) that will be propagated to screens declared in the Flows
- Stepper: a Stepper can be anything that can emit Steps inside Flows.
- Presentable: it is an abstraction of something that can be presented (basically UIViewController and Flow are Presentable).
- FlowContributor: it is a simple data structure that tells the FlowCoordinator what will be the next things that can emit new Steps in a Flow.
- FlowCoordinator: once the developer has defined the suitable combinations of Flows and Steps representing the navigation possibilities, the job of the FlowCoordinator is to mix these combinations to handle all the navigation of your app. FlowCoordinators are provided by RxFlow, you don't have to implement them.
How to use RxFlow
Code samples
How to declare Steps
Steps are little pieces of states eventually expressing the intent to navigate, it is pretty convenient to declare them in a enum:
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
}
The idea is to keep the Steps navigation independent as much as possible. For instance, calling a Step showMovieDetail(withId: Int) might be a bad idea since it tightly couples the fact of selecting a movie with the consequence of showing the movie detail screen. It is not up to the emitter of the Step to decide where to navigate, this decision belongs to the Flow.
How to declare a Flow
The following Flow is used as a Navigation stack. All you have to do is:
- Declare a root Presentable on which your navigation will be based.
- Implement the navigate(to:) function to transform a Step into a navigation actions.
Flows can be used to implement dependency injection when instantiating the ViewControllers.
The navigate(to:) function returns a FlowContributors. This is how the next navigation actions will be produced.
For instance the value: .one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: viewController.viewModel) means:
viewControlleris a Presentable and its lifecycle will affect the way the associated Stepper will emit Steps. For instance, if a Stepper emits a Step while its associated Presentable is temporarily hidden, this Step won't be taken care of.viewController.viewModelis a Stepper and will contribute to the navigation in that Flow by emitting Steps, according to its associated Presentable lifecycle.
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
