Swiftpack.co - Package - RxSwiftCommunity/RxFlow

| RxFlow Logo |

| | -------------- | -------------- | | GitHub Actions | | | Frameworks | Carthage Compatible CocoaPods Compatible Swift Package Manager compatible | | Platform | Platform | | Licence | License |

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:

  • 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 temporarely 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.
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 Deeplinks

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.

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<Step> 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 usecase would be to handle the navigation permissions within an application.

Let's say we have a 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

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 eventuelly 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).

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

}

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.

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 straightfoward and happens in the 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
    }
}

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)
  • RxSwift to expose Steps as Observables the Coordinator can react to (Github RxSwift)
  • Reusable in the Demo App to ease the storyboard cutting into atomic ViewControllers (Github Reusable)

Github

link
Stars: 1356

Dependencies

Used By

Total: 0

Releases

Galaxy - 2020-10-11 01:07:41

This Release:

  • fixes an issue with SPM
  • introduce the possibility to listen for steps event if a VC is dismissed (used for UIPageViewController, see the demo app)

Yamato - 2020-09-28 00:35:18

This Release mostly focuses on the upgrade to Xcode12/Swift 5.3

Melbourne - 2020-09-10 01:18:33

This release:

  • updates the Jazzy doc
  • fixes broken links in the Readme
  • fixes the behaviour of CompositeStepper (readyToEmitSteps() was not called for inner steppers) (https://github.com/RxSwiftCommunity/RxFlow/pull/154)

Relativity - 2020-05-26 19:17:39

This release brings:

  • a way to push a step through all the Flows hierarchy from the FlowCoordinator. It can be useful for deep linking: flowCoordinator.navigate(to:)
  • a new syntax for navigating with multiple flows: Flows.whenReady() is deprecated. You should instead use: Flows.use(when:) (see the example in the Readme.md and the demo application).

Cochrane - 2020-04-30 13:36:05

This release:

  • bumps RxSwift to version 5.1.1
  • bumps to Swift 5.2.2

Saratoga - 2020-01-09 15:21:04

This release brings:

  • SwifLint rules improvements
  • An "adapt" feature in Flows
  • Imports sanitization
  • Documentation updates
  • Unit tests for Flows
  • Single.zip instead of Observable.zip

Faragut - 2019-11-15 01:36:34

  • It is now possible to bypass the default behaviour of a FlowCoordinator and make it listen for steps emitted while the associated presentable is not currently visible (see FlowContributor.contribute(withNextPresentable: Presentable, withNextStepper: Stepper, allowStepWhenNotPresented: Bool = false)
  • Makes framework use only extensions APIs for debug and archive

Yorktown - 2019-10-06 20:53:33

  • FlowContributor.contribute(withNext:): shortcut function in case we have a single actor that is a Presentable and also a Stepper
  • add SPM support (thanks to @MortyMerr)
  • switch to GitHub Actions for the CI
  • add a tech talk reference in the README file
  • bump to Swift 5.1

Khitomer - 2019-08-04 03:01:04

This release contains the following improvements:

  • CI is now performed by CircleCI
  • FlowContributors.multiple has been extended to handle all kinds of FlowContributor (https://github.com/RxSwiftCommunity/RxFlow/pull/121) thanks to @MortyMerr.

Stargazer - 2019-05-08 01:20:35

  • bump to RxSwift 5.0.1 (thanks to @rynecheow)

Intreprid - 2019-04-24 02:25:37

  • Bump to Swift 5.0.1

Discovery - 2019-04-04 00:08:32

  • enable code coverage
  • bump RxSwift to 4.5.0
  • bump swift version to 5.0

Enterprise - 2019-02-03 03:03:18

  • Coordinator has been renamed in FlowCoordinator
  • NextFlowItem and NextFlowItems have been renamed in FlowContributor and FlowContributors
  • The FlowContributors enum entry .end (withStepForParentFlow: Step) has been renamed in .end (forwardToParentFlowWithStep: Step)
  • The FlowContributor class has been converted to an enum
  • In an effort to minimize the usage of objc_get/setAssociatedObject, each custom Stepper must declare a stored property steps as a PublishRelay
  • To trigger an initial Step inside a Stepper, an initialStep property has to be implemented
  • The callback function readyToEmitSteps() has been added to a Stepper. It is called once the Stepper can emit Steps
  • The reactive step property of a Stepper has been renamed in steps to reflect the plurality of the sequence
  • FlowCoordinator has been totally rewritten to improve memory management
  • HasDisposeBag has been removed from the project as it was not mandatory in the implementation and is not really related to RxFlow
  • The RxFlowStep enum is now provided to offer some common steps that can be used in lots of applications
  • Some of the old data structure names are still usable but have been explicitly deprecated

Excelsior - 2018-11-28 04:20:04

fix: memory leaks when ending flows with sibling flows. See https://github.com/RxSwiftCommunity/RxFlow/issues/92

Reliant - 2018-10-10 23:19:51

  • fix an issue introduced by the commit https://github.com/RxSwiftCommunity/RxFlow/commit/f713eb1c1e0abcd268ceaf847fe17c037a4b3e1a
  • remove unwanted swiftlint warnings
  • improve/bump travis ci env and simulator

Kelvin - 2018-09-20 00:53:14

  • fix UIViewController dismiss ControlEvent
  • change subscribe to a step from viewDidAppear to viewWillAppear
  • fix typo in README
  • fix typo in Presentable.swift
  • bump RxSwift version to 4.3
  • bump Swift version to 4.2

Defiant - 2018-08-31 00:41:24

  • use internally a UUID to identify Flows inside the Coordinator
  • bump RxSwift version to 4.2.0
  • fix a bug about dismissed Observable in UIViewController
  • demo project improvements / cleanup
  • fix typos
  • nextPresentable and nextStepper are now public to ease unit tests implementation

Add the .triggerParentFlow NextFlowItem + minor changes - 2018-07-01 02:14:55

This release introduces a new NextFlowItem: .triggerParentFlow. It allows to communicate with the parent flow without having to end the current flow.

This release also:

  • migrates the code to Swift 4.1.2
  • removes unnecessary references to UIKit

Migrate to Xcode 9.3 and Swift 4.1 - 2018-04-29 23:14:36

This release only adapts RxFlow to Xcode 9.3 and Swift 4.1

Prevent Reentrancy and unowned reference issues - 2018-03-26 23:54:01

The release is a bug fix release:

  • fix a RxSwift reentrancy issue due to a Flow dismissal
  • fix an 'unowned reference' reading error due to a Flow dismissal
  • fix the UIWindow Presentable extension so the windowDidAppear is now an Observable (not a Single)

Introduce a new .end NextFlowItems to simplify Flow dismissal - 2018-03-11 23:02:49

This release:

  • introduces a new NextFlowItems value called .end. It allows to be more explicit about how/when a Flow should be dismissed. It also allows to pass data from the dismissed Flow to its parent Flow.
  • updates the Demo App to implement new Flows AppFlow and OnBoardingFlow that take advantage of the new .end NextFlowItems value. The Demo App also demonstrates how to swap 2 Flows attached to the root Window.

Allows a Presentable as the root of a Flow + Bug fix - 2018-02-23 03:18:00

This release:

  • fixes a rx dismiss event problem on UIViewController when an Alert was dismissed
  • allows a Presentable to be the root of a Flow instead of a UIViewController
  • changes the Rx extension of the Coordinator to bring more context

NextFlowItem, CompositeStepper, RxSwift improvements - 2018-01-29 21:40:14

This release brings:

  • RxSwift improvements regarding the subscription chains in the Coordinator
  • a fix for Carthage installation
  • a new naming for Flowable: NextFlowItem
  • a new return type for navigate(to:): NextFlowItems, an enum that wraps the return of NextFlowItem
  • a new type of Stepper: CompositeStepper, that allows to associate several Steppers to one Presentable in a NextFlowItem
  • the possibility to wait for the readiness of a collection of Flows (Flows.whenReady)
  • the consistency for Flow dismissal. A child Flow has to be dismissed by its parent, not by itself

See the "Migrating from v1.0.x to v1.1" section in the README.md file. The Demo Application has been updated as well to implement the new features.

Refactor folders for Carthage - 2018-01-10 16:18:44

This release moves the RxFlow project to the root folder. When someone imported the project via Carthage, this compilation failed because Carthage was not able to compile the project with its dependencies as the Cartfile was not in the root folder.

Improve Rx safety with BehaviorRelay - 2018-01-05 01:52:44

Replace BehaviorSubject by BehaviorRelay. A BehaviorRelay cannot emit an error (it makes no sens to emit an error when emitting a new navigation state)

Add iOS 9.0 compatibility - 2018-01-04 00:30:26

This release sets the deployment target to iOS 9.0

Fix DemoStep filename - 2017-12-31 22:04:58

This release renames DemoWeft.swift in DemoStep.swift

First release since transfer from twittemb/Weavy - 2017-12-31 19:58:53

Fully functional Reactive Flow Coordinator.