Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/rxswiftcommunity/rxflow

RxFlow is a navigation framework for iOS applications based on a Reactive Flow Coordinator pattern
https://github.com/rxswiftcommunity/rxflow

coordinator flow reactive-programming rxswift swift

Last synced: 2 days ago
JSON representation

RxFlow is a navigation framework for iOS applications based on a Reactive Flow Coordinator pattern

Awesome Lists containing this project

README

        

| RxFlow Logo |

|
| -------------- | -------------- |
| GitHub Actions | ![](https://github.com/RxSwiftCommunity/RxFlow/workflows/Tests/badge.svg) |
| Frameworks | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/RxFlow.svg?style=flat)](http://cocoapods.org/pods/RxFlow) [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) |
| Platform | [![Platform](https://img.shields.io/cocoapods/p/RxFlow.svg?style=flat)](http://cocoapods.org/pods/RxFlow) |
| Licence | [![License](https://img.shields.io/cocoapods/l/RxFlow.svg?style=flat)](http://cocoapods.org/pods/RxFlow) |

# 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:
- [RxFlow Part 1: In Theory](https://sideeffect.io/posts/2017-11-08-rxflow-part1/)
- [RxFlow Part 2: In Practice](https://sideeffect.io/posts/2017-12-09-rxflow-part2/)
- [RxFlow Part 3: Tips and Tricks](https://sideeffect.io/posts/2017-12-22-rxflow-part3/)

The Jazzy documentation can be seen here as well: [Documentation](http://community.rxswift.org/RxFlow/)

Also here is a [Reactive coordinators tech talk](https://youtu.be/b0aCv7rlKx4) 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:

```ruby
github "RxSwiftCommunity/RxFlow"
```

## CocoaPods

In your Podfile:

```ruby
pod 'RxFlow'
```

## Swift Package Manager

In your Package.swift:

```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](http://khanlou.com/2015/10/coordinators-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:

```swift
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:
- ```viewController``` is 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.viewModel``` is a **Stepper** and will contribute to the navigation in that **Flow** by emitting **Steps**, according to its associated **Presentable** lifecycle.

```swift
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
}
}
```

### How to handle deep links

From the AppDelegate you can reach the FlowCoordinator and call the `navigate(to:)` function when receiving a notification for instance.

The step passed to the function will then be passed to all the existing Flows so you can adapt the navigation.

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

### How to adapt a Step before it triggers a navigation ?

A Flow has a `adapt(step:) -> Single` function that by default returns the step it has been given
as a parameter.

This function is called by the FlowCoordinator before the `navigate(to:)` function. This is a perfect place
to implement some logic that could for instance forbid a step to trigger a navigation. A common use case would be to handle the navigation permissions within an application.

Let's say we have a PermissionManager:

```swift
func adapt(step: Step) -> Single {
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
```

Why return a Single and not directly a Step ? Because some filtering processes could be asynchronous and need a user action to be performed (for instance a filtering based on the authentication layer of the device with TouchID or FaceID)

In order to improve the separation of concerns, a Flow could be injected with a delegate which purpose would be to handle the adaptions in the `adapt(step:)` function. The delegate could eventually be reused across multiple flows to ensure a consistency in the adaptations.

### How to declare a **Stepper**

In theory a **Stepper**, as it is a protocol, can be anything (a UIViewController for instance) but a good practice is to isolate that behavior in a ViewModel or something similar.

RxFlow comes with a predefined **OneStepper** class. For instance, it can be used when creating a new Flow to express the first **Step** that will drive the navigation.

The following **Stepper** will emit a **DemoStep.moviePicked(withMovieId:)** each time the function **pick(movieId:)** is called. The WatchedFlow will then call the function **navigateToMovieDetailScreen (with movieId: Int)**.

```swift
class WatchedViewModel: Stepper {

let movies: [MovieViewModel]
let steps = PublishRelay()

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

}
```

### Is it possible to coordinate multiple Flows ?

Of course, it is the aim of a Coordinator. Inside a Flow we can present UIViewControllers and also new Flows. The function **Flows.whenReady()** allows to be triggered when the new **Flow** is ready to be displayed and gives us back its root **Presentable**.

For instance, from the WishlistFlow, we launch the SettingsFlow in a popup.

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

The `Flows.use(when:)` takes an `ExecuteStrategy` as a second parameter. It has two possible values:
- .created: The completion block will be executed instantly
- .ready: The completion block will be executed once the sub flows (SettingsFlow in the example) have emitted a first step

For more complex cases, see the **DashboardFlow.swift** and the **SettingsFlow.swift** files in which we handle a UITabBarController and a UISplitViewController.

### How to bootstrap the RxFlow process

The coordination process is pretty straightforward and happens in the AppDelegate.

```swift
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
}
}
```

As a bonus, **FlowCoordinator** offers a Rx extension that allows you to track the navigation actions (**FlowCoordinator.rx.willNavigate** and **FlowCoordinator.rx.didNavigate**).

## Demo Application
A demo application is provided to illustrate the core mechanisms. Pretty much every kind of navigation is addressed. The app consists of:
- An AppFlow that represents the main navigation. This Flow will handle the OnboardingFlow and the DashboardFlow depending on the "onboarding state" of the user.
- An OnBoardingFlow that represents a 2 steps onboarding wizard in a UINavigationController. It will only be displayed the first time the app is used.
- A DashboardFlow that handles the Tabbar for the WishlistFlow and the WatchedFlow.
- A WishlistFlow that represents a navigation stack of movies that you want to watch.
- A WatchedFlow that represents a navigation stack of movies that you've already seen.
- A SettingsFlow that represents the user's preferences in a master/detail presentation.




Demo Application

# Tools and dependencies

RxFlow relies on:
- SwiftLint for static code analysis ([Github SwiftLint](https://github.com/realm/SwiftLint))
- RxSwift to expose Steps as Observables the Coordinator can react to ([Github RxSwift](https://github.com/ReactiveX/RxSwift))
- Reusable in the Demo App to ease the storyboard cutting into atomic ViewControllers ([Github Reusable](https://github.com/AliSoftware/Reusable))