Need help with FlowKit?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

FilipZawada
121 Stars 3 Forks MIT License 35 Commits 0 Opened issues

Description

Screenflow management for iOS

Services available

!
?

Need anything else?

Contributors list

# 72,971
HTML
CSS
fronten...
lodash
35 commits

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 ```text


| | | | | |
| TutorialVC | onContinue() | LoginVC | onLogin() | DashboardVC | | | -----------> | | --------> | | |_________| |___| |__________| ```

This is how any of our view controllers may look like: ```swift class DashboardViewController: UIViewController { var onBack: () -> Void = {} var onLogOut: () -> Void = {}

@IBAction func backButtonTapped(button: UIButton) {
    onBack()
}

@IBAction func logOutButtonTapped(button: UIButton) { onLogOut() }

} ```

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

  1. With interactions (short version)

    let yourScreen = Flow(with: YourViewController()) { vc, lets in
       vc.onBack = lets.pop()
       vc.onAbout = lets.present(otherScreen)
    }
    
  2. With interactions (long version) ``` let yourScreen = Flow { 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: ```plain 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.

```swift class MainFlow { lazy var tutorialScreen: Flow = 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.
.

Testability

under construction

I'd like to create a custom nimble matchers, so testing our flows would be as easy as writing them: ```swift let mainScreen = Flow(with: MainViewController()) mainScreen.letsFactory = LetsSpyFactory() let spy = mainScreen.letsFactory.makeSpy()

let vc = mainScreen.viewController

expect(vc.onBack).to(haveBeenBackedWith(spy.pop)) expect(vc.onLogOut).to(haveBeenBackedWith(spy.popTo(loginScreen))) expect(vc.onExit).to(haveBeenBackedWith(spy.popToRoot())) ```

Extensions

under construction

FlowKit shall integrate well with:

  • RxSwift (in preparation)
    let tutorialScreen = Flow(with: TutorialViewController()) { vc, lets in
        vc.onContinue
          .asDriver()
          .drive(lets.push(loginScreen))
          .addToDisposeBag(disposeBag)
    }
    ```  
  This is just an illustration, I'm not yet sure how this is going to look like.

About

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.

License

To be chosen soon.

designed in Poland, assembled in Swift 🙃

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.