Screenflow management for iOS





GitHub Stars



Last Commit

5yrs ago










Build Status codecov

Flow Kit

FlowKit iOS/Swift

Define screen flows easily with FlowKit. Elegant syntax, clear separation of concerns and testability makes it a perfect add-on for your current MV* setup.

let tutorialScreen = Flow(with: TutorialViewController()) { vc, lets in
    vc.onContinue = lets.push(loginScreen)
let loginScreen = Flow(with: LoginViewController()) { vc, lets in
    vc.onLogin = lets.push(dashboardScreen)
    vc.onBack = lets.pop()
let dashboardScreen = Flow(with: DashboardViewController()) { vc, lets in
    vc.onBack = lets.pop()
    vc.onLogOut = lets.popToRoot()

This supports following flow

 ____________                _________             _____________
|            |              |         |           |             |  
| TutorialVC | onContinue() | LoginVC | onLogin() | DashboardVC |
|            | -----------> |         | --------> |             |
|____________|              |_________|           |_____________|

This is how any of our view controllers may look like:

class DashboardViewController: UIViewController {
    var onBack: () -> Void = {}
    var onLogOut: () -> Void = {}
    @IBAction func backButtonTapped(button: UIButton) {
    @IBAction func logOutButtonTapped(button: UIButton) {

Defining a flow

Flow is a wrapper around any UIViewController. Through Flow you can define how your ViewController interacts with other view controllers. Main interactions possible are:

  • push(otherViewController)
  • present(otherViewController)
  • pop()
  • dismiss()

This approach has a few major advantages, since your ViewController:

  • doesn't need to know other view controllers in the town #loosely-coupling
  • focuses on managing it's own view, rather than managing apps navigation #single-responsibility
  • is easier to test #testability
  • code becomes more readable, since navigation-related pieces can be put in one place #readability
  • entrance and exit points of your ViewController are clearly defined #clear-api

There are multiple ways how to initialize the flow:

  1. Without interactions:

    let yourScreen = Flow(with: YourViewController())
    // or e.g. with a custom xib
    let yourScreen = Flow(with: YourViewController(nibName: "YourView", bundle: nil))

    Note that thanks to @autoclosure YourViewController() is instantiated lazily, i.e. only when needed. #swiftmagic

  2. With interactions (short version)

    let yourScreen = Flow(with: YourViewController()) { vc, lets in
        vc.onBack = lets.pop()
        vc.onAbout = lets.present(otherScreen)
  3. With interactions (long version)

    let yourScreen = Flow<YourViewController> { lets in
        // let's initialize our ViewController from a storyboard 
        let storyboard = UIStoryboard(name: "YourView", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "YourView") as! YourViewController
        vc.onBack = lets.pop()
        vc.onAbout = lets.present(otherScreen)
        return vc

Passing arguments

So, let's assume we've got a shopping app. ItemViewController presents our GreatProduct™. If a user decides to purchase it, CheckoutViewController is pushed to the screen to guide user through the checkout process. Now, how can CheckoutViewController know which product is actually being purchased? Obviously, it should receive that info from ItemViewController. This is how to do this:

class ItemViewController: UIViewController {
    var item: Item? // = GreatProduct™
    var quantity = 0
    var onCheckout: (Item, Int) -> Void?
    @IBAction func checkout(button: UIButton) {
        if let item = item, onCheckout = onCheckout {
            onCheckout(item, quantity)        

class CheckoutViewController: UIViewController {
    func prepare(item: Item, quantity: Int) {
        print("User wants to purchase \(item) × \(quantity)")

// our flow

let checkoutScreen = Flow(with: CheckoutViewController(nibName: "CheckoutView", bundle: nil))

let itemScreen = Flow(with: ItemViewController()) { vc, lets in

    vc.onCheckout = lets.push(checkoutScreen) { $0.prepare }

The trick here is to forward arguments from onCheckout() to prepare() function. This is done exactly here vc.onCheckout = lets.push(checkoutScreen) { $0.prepare } In plain words we'd say:

                   vc.  onCheckout= lets.push( checkoutScreen){$0.prepare }
hey itemViewController, on checkout lets push checkout screen and prepare it

Important thing to note here is that the signature of onCheckout has to be identical with the signature of prepare, so arguments can be passed successfully.

Grouping Flows

In a usual scenario it's convenient to group your flows in a separate classes. For example you can have a LoginFlow, SignUpFlow, CheckoutFlow etc... If your app is small, it may be enough to have one MainFlow.

class MainFlow {
    lazy var tutorialScreen: Flow<TutorialViewController> = Flow { [unowned self] lets in
        let screen = TutorialViewController()
        screen.onContinue = lets.push(self.loginScreen) { $0.prepare }
        return screen

    lazy var dashboardScreen: Flow<DashboardViewController> = Flow { [unowned self] lets in
        let screen = DashboardViewController()
        screen.onBack = lets.pop()
        screen.onLogOut = lets.popTo(self.loginScreen)
        screen.onExit = lets.popToRoot()
        return screen
    lazy var loginScreen: Flow<LoginViewController> = Flow { [unowned self] lets in
        let screen = LoginViewController()

        screen.onLogin = lets.push(self.dashboardScreen)
        screen.onBack = lets.pop()

        return screen

note 1. We had to use lazy var to allow dashboardScreen to reference loginScreen and vice versa. With regular let compiler wouldn't allow us to use loginScreen in dashboardScreen.

note 2. Unfortunately due to compiler bug we have declare variable type, otherwise we can't use self..


under construction

I'd like to create a custom nimble matchers, so testing our flows would be as easy as writing them:

let mainScreen = Flow(with: MainViewController())
mainScreen.letsFactory = LetsSpyFactory()
let spy = mainScreen.letsFactory.makeSpy()

let vc = mainScreen.viewController



under construction

FlowKit shall integrate well with:

  • RxSwift (in preparation)

      let tutorialScreen = Flow(with: TutorialViewController()) { vc, lets in

    This is just an illustration, I'm not yet sure how this is going to look like.


This project is created and maintained by Filip Zawada. It was created as a remedy for navigation problems in my last apps.

This projects is the next iteration over the idea of Flow Controllers, described by Krzysztof Zabłocki.


To be chosen soon.

designed in Poland, assembled in Swift 🙃

Rate & Review

Great Documentation0
Easy to Use0
Highly Customizable0
Bleeding Edge0
Responsive Maintainers0
Poor Documentation0
Hard to Use0
Unwelcoming Community0