Git Product home page Git Product logo

rxflow's Introduction

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

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 use case 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 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).

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

rxflow's People

Contributors

benjohnde avatar benmaer-parkwood avatar clsk avatar dangthaison91 avatar dchohfi avatar dimentar avatar dmanuelcl avatar edon2005 avatar freak4pc avatar funct7 avatar grafele avatar gruppio avatar james-lai-fpg avatar jordanekay avatar m0rtymerr avatar mashe avatar mgray88 avatar mtfum avatar mudox avatar onsissond avatar r-mckay avatar redryerye avatar ruslana003 avatar rynecheow avatar sebastianv1 avatar sirvon avatar sree127 avatar tomisacat avatar twittemb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rxflow's Issues

RxFlow stops working if a UIAlertController is presented by a flow's root

Hello, first of all congratulate you for your excellent work with RxFlow, I am integrating it in my project and the problem is given when the UIViewController root of a flow presents a UIAlertController, after the UIAlertController is dismissed these lines are executed in Coordinador.swift
captura de pantalla 2018-03-20 a la s 6 18 07 p m
It is called and therefore the flow ends, this issues no happen if the UIAlertController is presented by other UIViewController

excuse my English

Sorry this issue was solved with this pull request #27

RxFlowDemo nested NavigationControllers

Hi, first of all thank you for your afford and time creating this interesting library. After playing around in RxFlowDemo I couldn't find a way how to "switch flows" without pushing them to the "MainFlow root".

I mean, in your app you have this structure:
UINavigationController (MainFlow)
--UIViewController(apiKey)
--UITabBarController (apiKeyIsComplete)
----UINavigationController(WishlistWarp)
----UINavigationController(WatchedFlow)

What I dont like is, that you have nested UINavigationControllers and you have UITabBarController inside UINavigationController and then you have UINavigationController for every tab. I don't think this is best practice (ScrollViewInsets are weird that way and acording to documentation you UITabBar is meant to be root).

I would rather create "APIFlow" with root (UINavigationContorller) and then I would have "DashboardFlow" with root (UITabBarController) and when "APIFlow" is finished I would like my UITabBarController to be as a root in my hierarchy. So i would like to somehow swap UIWindow's rootViewController from the flow's perspective.

Im not sure If I described my point well enough. If you will agree, I can help afterwards.

`Flows.whenReady` not work.

Here's my MainFlow.swift:

final class MainFlow: Flow {

  var root: UIViewController {
    return self.rootViewController
  }

  private let rootViewController: UITabBarController

  init() {
    self.rootViewController = UITabBarController().then {
      $0.tabBar.isTranslucent = false
      $0.tabBar.barTintColor = .rh_primary
    }
  }

  func navigate(to step: Step) -> [NextFlowItem] {
    guard let step = step as? AppStep else { return NextFlowItem.noNavigation }

    switch step {
    case .main:
      return navigationToMainScreen()
    default:
      return NextFlowItem.noNavigation
    }
  }

  private func navigationToMainScreen() -> [NextFlowItem] {

    let firstFlow = FirstFlow()
    let secondFlow = SecondFlow()
    let thirdFlow = ThirdFlow()
    let fourthFlow = FourthFlow()

    Flows.whenReady(flows: [firstFlow, secondFlow, thirdFlow, fourthFlow]) { roots in
      self.rootViewController.setViewControllers(
        roots.map { root in
          return (root as! UINavigationController).then {
            $0.navigationBar.isTranslucent = false
            $0.navigationBar.barTintColor = .rh_primary
          }
        },
        animated: false)

        // test the function works or not!
        print("whenReady")
    }

    return [
      NextFlowItem(nextPresentable: firstFlow, nextStepper: OneStepper(withSingleStep: AppStep.first)),
      NextFlowItem(nextPresentable: secondFlow, nextStepper: OneStepper(withSingleStep: AppStep.second)),
      NextFlowItem(nextPresentable: thirdFlow, nextStepper: OneStepper(withSingleStep: AppStep.third)),
      NextFlowItem(nextPresentable: fourthFlow, nextStepper: OneStepper(withSingleStep: AppStep.fourth))]
  }
}

And my Podfile:

pod 'RxFlow', :git => 'https://github.com/RxSwiftCommunity/RxFlow.git', :branch => 'develop'

I found that the function whenReady is not work, and did not throw any errors. But when I change pods to commit: c488cdd like this:

pod 'RxFlow', :git => 'https://github.com/RxSwiftCommunity/RxFlow.git', :commit => 'c488cdd'

While changing NextFlowItem to Flowable, noNavigation to noFlow, it just works fine, and the console print whenReady.

I have checked the source code, but not find the reason.

Help me, please.

self.step.accept() not working while [pushViewController animated:true]

Hi. In our project we are trying to handle network errors in viewModels like that:

             apiClient.getTask()
                .asObservable()
                .catchError{ (error) -> Observable<Task> in
                    guard let error = error as? MoyaError else { return }
                    guard case .statusCode(let response) = error else { return }
                    
                    if response.statusCode == 401 {
                        print("\(response.statusCode) Unauthorized!")
                        invalidateUser()
                        self.step.accept(MySteps.auth)
                    }
                }

In Flow we are using that code for pushing controller:

    private func navigateToTask(withId identifier:String) -> NextFlowItems {
        …
        self.rootViewController.pushViewController(taskViewController, animated: true)
        return NextFlowItems.one(flowItem: NextFlowItem(nextPresentable: taskViewController, nextStepper: taskViewModel))
    }

So, error throws in the middle of animation of pushing view controller and step.accept() did not work. On the other hand, when we are pushing VC without animation (self.rootViewController.pushViewController(taskViewController, animated: false)) – its ok.

What are we doing wrong?

Reentrancy anomaly was detected rxflow

I received this warning whenever I nav toNextFlowItems.end(withStepForParentFlow: MZStep.XXX)

⚠️ Reentrancy anomaly was detected.
  > Debugging: To debug this issue you can set a breakpoint in /Users/derekxinzhewang/Companies/MobWorx/demo/MobCast/Pods/RxSwift/RxSwift/Rx.swift:97 and observe the call stack.
  > Problem: This behavior is breaking the observable sequence grammar. `next (error | completed)?`
    This behavior breaks the grammar because there is overlapping between sequence events.
    Observable sequence is trying to send an event before sending of previous event has finished.
  > Interpretation: This could mean that there is some kind of unexpected cyclic dependency in your code,
    or that the system is not behaving in the expected way.
  > Remedy: If this is the expected behavior this message can be suppressed by adding `.observeOn(MainScheduler.asyncInstance)`
    or by enqueing sequence events in some other way.

I have looked #24. But I didn't trigger step inside flow.

My Flow is simply modified version of RxFlowDemo

AppFlow.swift

class AppFlow: Flow {
    
    var root: Presentable {
        return self.rootWindow
    }
    
    private let rootWindow: UIWindow
    private let services: AppServices
    
    init(withWindow window: UIWindow, andServices services: AppServices) {
        self.rootWindow = window
        self.services = services
    }
    
    func navigate(to step: Step) -> NextFlowItems {
        guard let step = step as? MZStep else { return NextFlowItems.none }
        
        switch step {
        case .onboarding, .mainCompleted:
            print(".onboarding, .mainCompleted")
            return navigationToOnboardingFlow()
        case .onboardingCompleted, .main:
            print(".onboardingCompleted, .main")
            return navigationToMainFlow()
        default:
            return NextFlowItems.none
        }
    }
    
    private func navigationToOnboardingFlow() -> NextFlowItems {
        let onboardingFlow = OnboardingFlow(withServices: self.services)
        Flows.whenReady(flow1: onboardingFlow) { [unowned self] (root) in
            self.rootWindow.rootViewController = root
        }
        return NextFlowItems.one(flowItem: NextFlowItem(nextPresentable: onboardingFlow, nextStepper: OneStepper(withSingleStep: MZStep.signin)))
    }
    
    private func navigationToMainFlow() -> NextFlowItems {
        let mainFlow = MainFlow(withServices: self.services)
        Flows.whenReady(flow1: mainFlow) { [unowned self] (root) in
            self.rootWindow.rootViewController = root
        }
        return NextFlowItems.one(flowItem: NextFlowItem(nextPresentable: mainFlow, nextStepper: OneStepper(withSingleStep: MZStep.main)))
    }
    
}

struct AppServices {
}

class AppStepper: Stepper {
    init(withServices services: AppServices) {
        if !AWSSignInManager.sharedInstance().isLoggedIn {
            self.step.accept(MZStep.onboarding)
        } else {
            self.step.accept(MZStep.main)
        }
    }
}

OnboardingFlow.swift

class OnboardingFlow: Flow {
    
    var root: Presentable {
        return self.rootViewController
    }
    
    private lazy var rootViewController: UINavigationController = {
        let viewController = UINavigationController()
        return viewController
    }()
    
    private let services: OnboardingServices
    
    init(withServices services: AppServices) {
        self.services = OnboardingServices(services)
    }
    
    func navigate(to step: Step) -> NextFlowItems {
        guard let step = step as? MZStep else { return NextFlowItems.none }
        
        switch step {
        case .signin:
            return navigationToSigninScreen()
        case .validatePasscode(let user):
            return navigationToSigninPasscodeScreen(user)
        case .signinCompleted:
            print(".signinCompleted")
            return NextFlowItems.end(withStepForParentFlow: MZStep.onboardingCompleted)
        default:
            return NextFlowItems.none
        }
    }
    
    private func navigationToSigninScreen() -> NextFlowItems {
        let viewModel = MCRxSignInViewModel(services)
        let signinViewController = MCInputPhoneViewController.instantiate(viewModel)
        self.rootViewController.pushViewController(signinViewController, animated: false)
        return NextFlowItems.one(flowItem: NextFlowItem(nextPresentable: signinViewController, nextStepper: signinViewController))
    }
    
    private func navigationToSigninPasscodeScreen(_ user: AWSCognitoIdentityUser?) -> NextFlowItems {
        let viewModel = MCRxPasscodeViewModel(services)
        let passcodeViewController = MCValidatePhoneViewController.instantiate(viewModel, user: user)
        self.rootViewController.pushViewController(passcodeViewController, animated: true)
        return NextFlowItems.one(flowItem: NextFlowItem(nextPresentable: passcodeViewController, nextStepper: passcodeViewController))
    }
    
}

struct OnboardingServices {
    let authService: MZAuthService
    init(_ parentService: AppServices) {
        self.authService = MZAuthService()
    }
}

MainFlow.swift

class MainFlow: Flow {
    
    var root: Presentable {
        return self.rootViewController
    }
    
    private lazy var rootViewController: UINavigationController = {
        let viewController = UINavigationController()
        return viewController
    }()
    private let services: AppServices
    
    init(withServices services: AppServices) {
        self.services = services
    }
    
    func navigate(to step: Step) -> NextFlowItems {
        guard let step = step as? MZStep else { return NextFlowItems.none }
        
        switch step {
        case .main:
            return navigateToMainScreen()
        case .signOutCompleted:
            print(".signOutCompleted")
            return NextFlowItems.end(withStepForParentFlow: MZStep.mainCompleted)
        default:
            return NextFlowItems.none
        }
        
    }
    
    private func navigateToMainScreen() -> NextFlowItems {
        let mainViewController = MCMainViewController.instantiate()
        self.rootViewController.pushViewController(mainViewController, animated: true)
        return NextFlowItems.one(flowItem: NextFlowItem(nextPresentable: mainViewController, nextStepper: mainViewController))
    }
}

Also, I noticed willNavigate is not triggered after NextFlowItems.end.
For example, if I signin

did navigate to flow=Demo.OnboardingFlow and step=signinCompleted
Reentrancy warning
did navigate to flow=Demo.AppFlow and step=onboardingCompleted
will navigate to flow=Demo.MainFlow and step=main
did navigate to flow=Demo.MainFlow and step=main

Not sure if I did something wrong.
Thanks

Is Carthage mandatory?

There is a run script that expect the user to have a carthage (/usr/local/bin/carthage) folder. The projects won't build because of this

Always visible floating view and it's Steps

We have a view in our app, which is visible on each screen. This view can present some views modally or push them onto the navigation stack.

Any advice on how to create Flows in this scenario? It would be nice if all steps from this floating view would be handled by some RootFlow from where all the other views are presented, so the RootFlow would dismiss everything and present new view if needed.

carthage installation issue

With github "RxSwiftCommunity/RxFlow" in my path/to/myproject/Cartfile, carthage update[1] fails without any useful information in logs (both stdout and xcodebuild log file).

The solution was to cd /path/to/myproject/Carthage/Checkouts/RxFlow/RxFlow && carthage update[1]. After this, running carthage update[1] again in /path/to/myproject succeeded as expected.

It should not be ideally necessary for the developer to do a nested installation.

[1] carthage update --platform iOS --configuration Release --no-use-binaries --cache-builds

Navigate to next item within a flow

Hi there,

I was wondering if it is possible to navigate to the next flow item with a simple call like step.next. As far as I know and tried out several things, I always have to know what the next step should be. We have a big onboarding navigation where we just want to set the order of the steps within the step enum and then just want to call step.next or something similar.

Any ideas?

Many thanks,
FinDev

How to coordinate more than 3 Flows?

How to coordinate more than 3 Flows when using a UITabBarController with 4 or 5 tabBars?

I tried to overwrite the Flows.whenReady using extension, but it thrown error 'rxFlowReady' is inaccessible due to 'internal' protection level.

Flows.whenReady -> variable naming

I just wondered if there exists a specific reason for naming the variable in the function Flows.whenReady flow1 and not just flow for convenience? :)

I fully understand the notion when it comes to more than one flow.

We wrote a very little extension for that, cause we found it fresher to read:

extension Flows {
    public static func whenReady<RootType: UIViewController> (flow: Flow, block: @escaping (_ flowRoot: RootType) -> Void) {
        whenReady(flow1: flow, block: block)
    }
}

`dismissed` observable triggered without view being dismissed

Hey,

I really like your framework and we tried using it in one of our apps. Right now we are facing a problem when showing a simple alert view and because of that I started investigating what was going on in the framework and I think I found a bug in the dismissed observable causing multiple other problems.

How to reproduce:
Show an alert in an UINavigationController
Dismissing the alert leads to an event being fired for the dismissed observable of the UINavigationController.

This leads to multiple other issues, because the 'rxDismissed' observable will be fired therefore as well for the flow and our flow doesn't react to any event anymore even tho this view is still there.

UIAlertController Handling

I'm starting a new project and I try to use RxFlow 😃

I cant find a simple solution how to best handle UIAlertControllers, without a taste of over engineering my flows. Can you give me some advise how to best handle them?

  • Do you create a general Step like "openAlert(title, message, actions)"? This could be implemented in all flows (or some kind of base class).
  • Do create a step per alert?
  • Do even create steps for alerts or do you just handle alerts in the controllers?
  • Do you have a different approach?

Thanks in advance!

How to hide a tab bar when using RxFlow?

Hi,

I am trying to use RxFlow (I am new to RxSwift too) for a new project and I would like to hide a tab bar. The only solution I found so far is to have a global variable like
let DashboardRootViewController = UITabBarController()

then DashboardRootViewController.tabBar.isHidden = true

Is there a nicer way to do it?

Handling Deep Links

Hello!

I am trying to get Deep Linking to work with RxFlow. I have an AppStepper that has a method attemptNavigation(to:DeepLinkable) -> Bool which gets called for example when a link with the apps URL scheme is clicked:

extension AppDelegate {
    func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
        guard let appStepper = appStepper else { return false }
        return appStepper.attemptNavigation(to: url)
    }
}

My AppStepper implementation looks as follows where DeepLinkService is responsible for converting DeepLinkable items to Step:

final class AppStepper: Stepper {
    private let deepLinkService: DeepLinkServiceProtocol
    private let container: Container
    init(container: Container, deepLinkService: DeepLinkServiceProtocol) {
        self.deepLinkService = deepLinkService
        self.container = container
        step(to: initialStep(container: container))
    }
}

//MARK: - Deep Link Handling
extension AppStepper {
    @discardableResult func attemptNavigation(to maybeDeepLink: DeepLinkable) -> Bool {
        guard let deepLinkType = deepLinkService.parse(maybeDeepLink) else { return false }
        let _nextStep = nextStep(for: deepLinkType, in: container)
        step(to: _nextStep)
        return true
    }

    func nextStep(for deepLinkType: DeepLinkType, in container: Container) -> AppStep {
        let isLoggedIn = (container ~> KeyValueStore<KeyChainKey>.self).hasString(for: .token(.authentication))
        let nextStep = isLoggedIn ? deepLinkType.step : .authStart
        return nextStep
    }
}

Then I have the tabBar solution that @Sajjon implemented in this PR: #41 to show .multiple NextFlowItems for each tab after the user is logged in.

The problem I'm facing is that step(to: _nextStep) in attemptNavigation(to:DeepLinkable) never triggers the navigate(to:Step) method of my AppFlow and therefore I can't do any navigation from Deep Links.

Do you have any suggestions on how I can implement this kind of Deep Link handling?

Docs

Are the docs hosted anywhere? Haven't found the place yet :)

Btw really nice library, love how it helps to reduce the boilerplate in our platform.

Type AppFlow does not conform to protocol Flow

I cloned your example project, deleted Carthage support and created my own Podfile and tried to compile, but I occured an errors like this:
Type 'WatchedFlow' does not conform to protocol Flow and etc Flow's
2018-02-20 16 26 24

When I created a new empty project, linked pods, created my own Flow class like AppFlow, I also occured this error.

My environment:

  • Xcode 9.2
  • Swift 4

My Podfile:

use_frameworks!
target 'RxFlowDemo' do
  pod 'RxSwift'
  pod 'RxCocoa'
  pod 'RxFlow'
  pod 'Reusable'
end

What I'm doing wrong?

Passing data throughout the Flow.

In this flow, in particular the func navigate(to:)
I'm passing data like, 3 times...

Is there anyway to avoid passing data so many times? like here:

func navigate(to step:Step) -> NextFlowItems {
        guard let step = step as? AppStep else { return NextFlowItems.stepNotHandled }
        
        switch step {
        case .destinationsNearMe(let lat, let long):
            return navigateToDestinationsNearMeList(with: lat, with: long)
        default:
            return NextFlowItems.stepNotHandled
        }
    }
    
    private func navigationToDashboardScreen() -> NextFlowItems {
        
    }
    
    private func navigateToDestinationsNearMeList(with lat: Double, with long: Double) -> NextFlowItems {
       

This same data is being passed three times,

        case .destinationsNearMe(let lat, let long):
            return navigateToDestinationsNearMeList(with: lat, with: long)
func navigateToDestinationsNearMeList(with lat: Double, with long: Double)

can you recommend or show an example of an RX way or a more efficient way to pass the data,
from the VC to the VM then to the Services/Data Providers/Networking, that need some
current user/device state info, to kick off processing.

What's the difference in these two navigate(to:) impl?

Is it just merely style?

from: SettingsFlow.swift

func navigate(to step: Step) -> NextFlowItems {
        guard let step = step as? DemoStep else { return NextFlowItems.stepNotHandled }

        switch step {
        case .settings:
            let navigationController = UINavigationController()
            let settingsListViewController = SettingsListViewController.instantiate()
            let settingsViewController = SettingsViewController.instantiate()

            self.rootViewController.viewControllers = [navigationController, settingsViewController]
            self.rootViewController.preferredDisplayMode = .allVisible

            settingsViewController.title = "Api Key"

            navigationController.viewControllers = [settingsListViewController]
            if let navigationBarItem = navigationController.navigationBar.items?[0] {
                let settingsButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.done,
                                                     target: self.settingsStepper,
                                                     action: #selector(SettingsStepper.settingsDone))
                navigationBarItem.setRightBarButton(settingsButton, animated: false)
            }

            return NextFlowItems.multiple(flowItems: [NextFlowItem(nextPresentable: settingsListViewController, nextStepper: settingsListViewController),
                                                      NextFlowItem(nextPresentable: settingsViewController, nextStepper: settingsViewController)])
        case .apiKey:
            let settingsViewController = SettingsViewController.instantiate()
            settingsViewController.title = "Api Key"
            self.rootViewController.showDetailViewController(settingsViewController, sender: nil)
            return NextFlowItems.one(flowItem: NextFlowItem(nextPresentable: settingsViewController, nextStepper: settingsViewController))
        case .about:
            let settingsAboutViewController = SettingsAboutViewController.instantiate()
            settingsAboutViewController.title = "About"
            self.rootViewController.showDetailViewController(settingsAboutViewController, sender: nil)
            return NextFlowItems.one(flowItem: NextFlowItem(nextPresentable: settingsAboutViewController, nextStepper: settingsAboutViewController))
        default:
            return NextFlowItems.stepNotHandled
        }

from: MainFlow.swift

    func navigate(to step: Step) -> NextFlowItems {
        guard let step = step as? DemoStep else { return NextFlowItems.stepNotHandled }

        switch step {
        case .apiKey:
            return navigationToApiScreen()
        case .apiKeyIsComplete:
            return navigationToDashboardScreen()
        default:
            return NextFlowItems.stepNotHandled
        }
    }

Few source of stepper...

Hi Guys,

Thanks for your framework!
And I am a little confused how to solve a problem:
I have a MainViewController with child view controller inside.
Also the main ViewController have a top left button clicking on showed LeftMenu.
the problem is next: user can click on buttons inside child view controller that should set next step,
and user can click on buttona inside left menu, that should set the same next step as inside child view controller.
On clicking button the information should changed in childviewcontroller. No metter where button was clicked.
I set in code next string:

return NextFlowItems.multiple(flowItems: [
            NextFlowItem(nextPresentable: modulesFlow, nextStepper: leftMenuViewController),
            NextFlowItem(nextPresentable: modulesFlow, nextStepper: OneStepper(withSingleStep: QRStep.module(0))),
            ]
        )

But with such code clicking inside children view controller works good, but clicking on buttons in Left menu rxflow did not see. How can I achieve needed result?

Thanks.

No such module RxFlow

Everytime, i try to build a Demo Project or a Project I create with RxFlow, I get

No module 'RxFlow'

I'm using cocoapods

  pod 'RxFlow'

It took me a little bit to understand the concepts behind this library and integrate them into my
projects, the time has come to build. But, I am now facing issues getting RxFlow to import.

I have tried using cocoapods and Carthage, on fresh projects, existing projects and the demo project, to no avail.

Add willNavigate: Observable<Flow,Step> and didNavigate: Observable<Flow,Step> to Coordinator

Hi,
Currently the coordinator object has only 2 rx streams (willNavigate, didNavigate) that are specialized with 2 Strings that describes the Flow and the Step.

var willNavigate: Observable<(String, String)>
var didNavigate: Observable<(String, String)>

These are very useful for "Analytics purposes".

My idea is to add 2 more streams, that are basically the same but instead to have 2 Strings as specialized Types, they will have a Flow and a Step

var willNavigate: Observable<(Flow, Step)>
var didNavigate: Observable<(Flow, Step)>

These streams are very powerful because allow the coordinator to act on the Flow or on the Step during the Navigation. For example the coordinator can then set a specific property by checking Protocol Conformance acting like as a "Dependency Resolver".

Moreover this extend the analytics capabilities of the current solution. So it's possible to have a custom logging convention per Flow or per Step.

If you need more detailed explanation don't hesitate to contact me 😃

Temporary Flows which root which is neither presented nor pushed - how to dismiss?

I want to show several temporary flows before I will show the MainFlow (user signed in and show the main screen in the app).

The AppDelegate's Coordinator coordinates to the AppFlow in application:didFinishLaunchingWithOptions.

The AppFlow will then navigate to several temporary flows before MainFlow, where each Flow has several screens (steps)

Lets say that we have these flow chart:
AppStart> AppFlow --set-root--> FirstRunFlow --set-root--> SignInFlow --set-root--> MainFlow

I do not want the user to be able to navigate back from SignInFlow to FirstRunFlow. What makes sense is to completely replace the NavigationStack (the UINavigationController and its UIViewControllers) of SignInFlow with a new UINavigationController when SignInFlow is done (last step reached) .

So for the temporary flows I guess I want to replace the window in AppDelegate with each temporary flows' UINavigationController.

I have created an example app but it is not working fully because I have not really been able to dismiss these temporary flows. Their deinit does not get called.

In the README it says

In order to improve RxFlow consistency, it seemed obvious that a Flow that presents a UIViewController also HAS to dismiss it. In previous versions, a child Flow was dismissed by itself and not by the Flow that presented it. In the Demo Application you will find an example with the WishlistFlow dismissing the SettingsFlow for the settingsDone Step.

This sounds very relevant to what I want to do. I WANT to dismiss a Flow manually I guess? Because right now the framework seems to be working fine when Presentables are presented or pushed.

How can I handle the scenario where I have several temporary Flows I want to go through but disabling the possibility of navigating back and ultimately land on a MainFlow where the user spends most of her time (logged in and consuming content).

My example app is rather ambitious, please have a look: https://github.com/Sajjon/RxFlowExample

What to do? :)

How could I implement a custom action for a UITabBarItem?

I reviewed the code from RxFlowDemo, but there's no example of how to handle a custom action from a UITabBarController.

To handle a custom action we commonly implement the following function from UITabBarControllerDelegate:

    func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
        
        if viewControllers?.index(of: viewController) == 2 {
            // TODO: Open camera from TabCoordinator
            return false
        }
        
        return true
    }

How would this be done in RxFlow?

The best practice to handle nested flows

My app has tab bar navigation with two tabs (Profile and Feed).

AppFlow class implement code for tabBar:

class AppFlow: Flow {
    var root: Presentable {
        return self.rootWindow
    }

    private let rootWindow: UIWindow!
    private let tabBarController: UITabBarController!

    init(with window: UIWindow) {
        self.rootWindow = window
        self.tabBarController = UITabBarController()
    }

    func navigate(to step: Step) -> NextFlowItems {
        guard let step = step as? MyStep else {
            return NextFlowItems.none
        }

        switch step {
        case .introComplete:
            return navigationToMain()
        default:
            return NextFlowItems.none
        }
    }

    private func navigationToMain() -> NextFlowItems {
        let feedFlow = FeedFlow()
        let profileFlow = ProfileFlow()
        Flows.whenReady(flow1: feedFlow, flow2: profileFlow, block: { [unowned self] (root1: UINavigationController, root2: UINavigationController) in
            let tabBarItem1 = UITabBarItem(title: "Profile", image: nil, selectedImage: nil)
            let tabBarItem2 = UITabBarItem(title: "Feed", image: nil, selectedImage: nil)
            root1.tabBarItem = tabBarItem1
            root2.tabBarItem = tabBarItem2
            self.tabBarController.setViewControllers([root1, root2], animated: false)
            self.rootWindow.rootViewController = self.tabBarController
        })

        return NextFlowItems.multiple(flowItems: [
            NextFlowItem(nextPresentable: profileFlow, nextStepper: OneStepper(withSingleStep: MyStep.profileShow)),
            NextFlowItem(nextPresentable: feedFlow, nextStepper: OneStepper(withSingleStep: MyStep.feedShowScreen))
        ])
    }
}

FeedFlow class implement navigation for feed:

class FeedFlow: Flow {
    var root: Presentable {
        return self.rootViewController
    }

    private let rootViewController = UINavigationViewController()

    func navigate(to step: Step) -> NextFlowItems {
        guard let step = step as? MyStep else {
            return NextFlowItems.none
        }

        switch step {
        case .feedPickNews(let news):
            return navigationToOpenNews(with: news)
        case .feedShowScreen:
            return navigationToShow()
        case .feedSaveNews:
            return NextFlowItems.end(withStepForParentFlow: MyStep.openProfileTab)
        default:
            return NextFlowItems.none
        }
    }

    private func navigationToShow() -> NextFlowItems {
        let feedViewController = StoryboardScene.Feed.feedViewController.instantiate()
        let feedViewModel = FeedViewModel()
        feedViewController.viewModel = feedViewModel
        self.rootViewController.pushViewController(feedViewController, animated: true)
        return NextFlowItems.one(flowItem: NextFlowItem(nextPresentable: feedViewController, nextStepper: feedViewModel))
    }

    private func navigationToOpenNews(with news: News) -> NextFlowItems {
        let viewController = StoryboardScene.Feed.feedInfoViewController.instantiate()
        let viewModel = FeedInfoViewModel(news: news)
        viewController.viewModel = viewModel
        self.rootViewController.pushViewController(viewController, animated: true)
        return NextFlowItems.one(flowItem: NextFlowItem(nextPresentable: viewController, nextStepper: viewModel))
    }
}

And I want to realize this flow:

Open app (Profile tab) –> Choose Feed tab –> Pick any news cell –> On News screen click button Save news –> ... and I want to dismiss navigation from Feed Info screen (.feedSaveNews) to root and locate Profile screen back.

First, I tried to finish (.end(...)) Feed flow, it works but further, I can't open any news in Feed. Then I tried to create one more flow for FeedInfoViewController, that I could be finished FeedInfoFlow, but I stuck with navigations problems.

Can you advise something for my case?

NoneStepper question

Hi. I just recently started looking at RxFlow and have a question. I see that in RxFlow/Stepper.swift that there is a class named NoneStepper, but that class is not public. I had a case tonight where I wanted to pass an empty Stepper object. I don't see that class being used anywhere in the framework. It seems weird to be private to the framework and not exposed. Is it meant to be public?

Great framework. Thanks for contributing it to the community.

Remove nested frameworks

The RxFlow framework should not contain nested frameworks!
At the latest during submitting to the AppStore, developers will prompted errors that RxFlow contains illegal nested frameworks.
To fix that disable the Copy Carthage Frameworks build phase for the framework.

bildschirmfoto 2018-02-22 um 13 30 54

bildschirmfoto 2018-02-22 um 13 42 18

Child View Controllers with own flows

Hi!

I wanted to ask if child view controllers with their own flows are currently supported by RxFlow. If I understand the source code right, the FlowCoordinator won't be removed until it's parent was dismissed. I can think about cases where the parent won't be dismissed at all and we add/remove child view controllers continuously. That would lead to a situation with many FlowCoordinators lying around without a way of deleting them.

Please correct me if I'm wrong.

Subscribe step

Hi guys,

From what I understand looking at code of RxFlow, step will be subscribed when viewController didAppear.
Why rxflow dont do it earlier, for example on viewWillAppear or more earlier?

Thanks.

Serious Memory Management Issues

A comment was left by @TobiasRe on this issue #25, about...

There are serious memory management issues with RxFlow to fix first.

Can anyone with production/ or app store release experience with this framework,
comment about any memory leaks they've discovered.

Thanks

Root window emit the completed event immediately after be shown

Hi in my current project I need to swap between 2 Flows attached to the root Window, in the main flow I keep checking the user signing status to decide if in need show one flow or other , but when I try to swap between flows the steps emited from the main flow are ignored becouse the window.rxVisible emit completed just after the window become keyAndVisible so the cordinator ignores that steps

Other Types of Steppers Impl

Thanks for this library, coming at a great time.

(for instance when we only need to bootstrap a Flow with a first Step and don't want to code a basic Stepper for that)

Where is the documentation on coding a basic Stepper?

flow deallocation when return .end on navigate(to:)

“Fatal error: Attempted to read an unowned reference but object 0x1c42d11e0 was already deallocated” is thrown when a child flow returns .end(withStepForParentFlow: step) on navigate(to:)

I have detected that the problem is caused by this line of code

self.delegate.endFlowCoordinator(withIdentifier: self.identifier)

In the following code snippet

captura de pantalla 2018-03-24 a la s 5 43 06 p m

The error is indicated in this line

captura de pantalla 2018-03-24 a la s 5 41 36 p m

What I understand is happening is that as you call

self.dismissFlow.onNext(Void ())

and immediately you call

self.delegate.endFlowCoordinator(withIdentifier: self.identifier)

The flow handled by the FlowCoordinator is deallocated in the latter so when the event is issued in dismissFlow the flow does not exist and therefore the error, the solution is to eliminate

122 self.delegate.endFlowCoordinator(withIdentifier: self.identifier)

Since it is also called in the subscription of dismissFlow
captura de pantalla 2018-03-24 a la s 5 41 36 p m

Apologies for my English

potential memory leak?

In Flow.swift there is a Flows class:

/// Utility functions to synchronize Flows readyness
public class Flows {

    // swiftlint:disable line_length
    /// Allow to be triggered only when Flows given as parameters are ready to be displayed.
    /// Once it is the case, the block is executed
    ///
    /// - Parameters:
    ///   - flow1: first Flow to be observed
    ///   - flow2: second Flow to be observed
    ///   - flow3: third Flow to be observed
    ///   - block: block to execute whenever the Flows are ready to use
    public static func whenReady<RootType1: UIViewController, RootType2: UIViewController, RootType3: UIViewController> (flow1: Flow,
                                                                                                                         flow2: Flow,
                                                                                                                         flow3: Flow,
                                                                                                                         block: @escaping (_ warp1Root: RootType1, _ warp2Root: RootType2, _ warp3Root: RootType3) -> Void) {
        _ = Observable<Void>.zip(flow1.rxFlowReady.asObservable(), flow2.rxFlowReady.asObservable(), flow3.rxFlowReady.asObservable()) { (_, _, _) in
            return Void()
            }.take(1).subscribe(onNext: { (_) in
                guard   let root1 = flow1.root as? RootType1,
                    let root2 = flow2.root as? RootType2,
                    let root3 = flow3.root as? RootType3 else {
                        fatalError ("Type mismatch, Flows roots types do not match the types awaited in the block")
                }
                block(root1, root2, root3)
            })
    }

    ......
}

is there no problem without caring the returned Disposable value?

RxReduce + RxFlow

Is there a way to combine RxReduce + RxFlow to get something like ReSwift ?
Having both well integrated would really enhance Rx project in my opinion.

navigate(to) fired multiple times

The navigate(to) fired multiple times if the nextStepper is set by a shared insance.
please check below code. The screen1 presented as expected, but when the viewModel set the state to the screen2, the navigate(to:) fired twice. It means the screen2 was pushed twice into the navigation stack.


final class RegistrationFlow: Flow {
    var root: Presentable {
        return self.rootViewController
    }

    let viewModel = SharedViewModel()
    private let rootViewController = UINavigationController()

    init() { }

    func navigate(to step: Step) -> NextFlowItems {
        guard let step = step as? DLStep else { return .none }
        switch step {
        case .screen1:
            return navigateToScreen1()
        case .screen2:
            return navigateToScreen2()
        case .screen3:
            return navigateToScreen3()
        default:
            return .none
        }
    }

    private func navigateToScreen1() -> NextFlowItems {
        let viewController = Screen1ViewController()!
        viewController.viewModel = viewModel

        rootViewController.setViewControllers([viewController], animated: false)
        return .one(flowItem: NextFlowItem(nextPresentable: viewController, nextStepper: viewModel))
    }

    private func navigateToScreen2() -> NextFlowItems {
        let viewController = Screen2ViewController()!
        viewController.viewModel = viewModel

        rootViewController.show(viewController, sender: self)
        return .one(flowItem: NextFlowItem(nextPresentable: viewController, nextStepper: viewModel))
    }
    
    private func navigateToScreen3() -> NextFlowItems {
        let viewController = Screen3ViewController()!
        viewController.viewModel = viewModel

        rootViewController.show(viewController, sender: self)
        return .one(flowItem: NextFlowItem(nextPresentable: viewController, nextStepper: viewModel))
    }
  
}

Global Stepper implementation

I really like the RxFlow approach for separating navigation with flows.
Which works really well with navigating step by step as the user uses the app gets complicated with deep linking (e.g.: Navigating to ViewController after receiving a push notification).

How do I implement a global stepper for deep linking?
The Stepper should do the following:

  1. Accept a specific Step
  2. Select or create and attach the corresponding Flow
  3. Add the Stepto the flow

Bundle returning target reference instead of module reference

Hi, first off—thanks creating RxFlow! I'm using it for the first time and it looks pretty good so far. I'm running into a weird issue that seems to be linked to returning a view controller as a NextFlowItem in a flow. If I try to load a nib programmatically at any initial stage of the view controller lifecycle (viewDidLoad, viewWillAppear, and viewDidAppear) and attempt to get a reference to our module's bundle by using Bundle(for: type(of: self)) (self being the view controller) a reference to our app's target is returned instead of our framework module.

Example:
From my flow function that returns NextFlowItems from a Step enum case:

private func navigateToMyNextScreen() -> NextFlowItems {
    // Create view controller, add view model, and push it with the nav controller
    let nextFlowItem = NextFlowItem(nextPresentable: myViewController, nextStepper: myViewController)
    return NextFlowItems.one(flowItem: nextFlowItem)
}

Output of at viewDidLoad, viewWillAppear, and viewDidAppear in myViewController from Bundle(for: type(of: self)).resourcePath:
"/Users/[...]/Library/Developer/CoreSimulator/Devices/01802E25-FD20-43A8-9729-831B620F0281/data/Containers/Bundle/Application/310DB524-2ACA-4FD6-9168-9A0102D43239/[...].app"

However, if I just return NextFlowItems.none from navigateToMyNextScreen() (i.e., the view controller isn't registered):

private func navigateToMyNextScreen() -> NextFlowItems {
    // Create view controller, add view model, and push it with the nav controller
    return NextFlowItems.none
}

And the correct bundle resourcePath is given:
Output at viewDidLoad, viewWillAppear, and viewDidAppear in myViewController from Bundle(for: type(of: self)).resourcePath:
"/Users/[...]/Library/Developer/Xcode/DerivedData/[...]-flzbuuwuhjeoxpeasujpzylznujt/Build/Products/Debug-iphonesimulator/[...].framework"

This issue appears similar to jverkoey/iOS-Framework#107, although from my understanding in Swift 4 type(of: self) is supposed to be equivalent to Obj-C [self class].

We're using RxFlow a little differently from the sample projects, so not sure if that could be causing it. As we're testing it out on a greenfield feature inside a mature app, we're not initializing it with the app's UIWindow; instead, we're using it in one tab of a UITabViewController and it's only one flow. The flow returns a UINavigationController as its root property which is then added to the tab that's displayed. We also have multiple frameworks that we use in the same project, but RxFlow is only being used inside one framework.

Is there anything you can think of in either RxFlow or our use case that could cause this to occur? It's easy enough to work around but was wondering what was causing it, as it's a rather weird bug. Thanks!

What is the need to initialize all the services in AppDelegate?

In the Demo app, all the services are being initialized in the AppDelegate and all the services are being passed to all the flows, even if it is not required by some flows. So, I just wanted to ask that what would be the best way to initialize and pass only the required services in a flow?

Thank you.

Spice up Step enum.

Hey, in this Example by @sajjon

I like the way he spices up the Steps enum.

enum AppStep: Step {
    
    case start // Never Ending Story
    
    indirect case first(First)
    static var firstStart: AppStep { return .first(.start) }
    enum First: Step {
        case start, done
        case applePay, applePayDone
        case permissions, permissionsDone
    }
    
    indirect case version(Version)
    static var versionStart: AppStep { return .version(.start) }
    enum Version: Step {
        case start, done
        case forceUpdate, forceUpdateBlock
        case onboarding, onboardingDone
    }
.....

case .version(.start): return navigateToVersionStartScreen()

when you're working with that enum
you type,

Flow(.step)

super clear!

Vs. the way original Steps

enum DemoStep: Step {
    case onboarding
    case apiKey
    case apiKeyIsComplete
    case login
    case loginIsComplete

    case dashboard
    case movieList

    case moviePicked (withMovieId: Int)
    case castPicked (withCastId: Int)

    case settings
    case settingsDone
    case about
}

Is there anyway to introduce this into the frameworks,

theres some parts in this branch, feature/onboardingFlow
I don't get enough to incorporate the two well, just yet...

Pattern for unit testing flows?

I'm using RxFlow in my app, and I'm admittedly behind on my unit test plans. I'm trying to catch up and create unit tests for my flows. When creating a unit test for the Flow.navigate(to:) method though, I'm seeing that there's no good way to really test the result. I can test whether the NextFlowItems result is one, none, or many, but there's no way to test to validate that the right flow or view controller is being returned because the members of NextFlowItem are not public.

Is there a recommended way for building unit tests involving NextFlowItem and NextFlowItems?

Passing needed dependencies as Step enum associated values

I was trying this library and was thinking about app flows and DI, so I have a question: is it bad practice to declare enum cases with associated values (view models) to pass them from Steppers? It will retain the most recent step's value because BehaviorRelay will hold it, but maybe it can be changed somehow, if the idea isn't bad, if it is please explain why.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.