Skip to content
Go back

Improve your iOS Architecture with FlowControllers

When working on iOS app, now more than ever one should avoid having view controllers pushing other view controllers around.

Why?

The problem

Modern apps need to support multiple presentation types for same VC, e.g., on iPhone you push a new view controller but on iPad you either embed it as a containment view controller or show it from popover.

Also in many cases you might want to reuse the same ViewController in different scenario, e.g. Image Picking View Controller can appear in multiple places and be presented differently depending on it’s context.

ViewControllers should be implemented in such a way that they don’t depend on their presentation style, after all this is one of the reasons we have size classes.

Let’s assume that you do naive version of presenting view controller from other VC’s / ViewModels, you will end up with a bunch of if statements and your code will be one big spaghetti of conditions.

Since my work as a Consultant often involved reviewing projects and helping teams establish cleaner solutions.

I’ve seen a lot of spaghetti code, this is not even close to the worst I’ve seen but it’s pretty bad already:

func doneButtonTapped() {
  let vc = NextViewController(prepareNeccesaryState())

  if Device.isIPad() {
    navigationController.pushViewController(vc, animated: true, completion: nil)
  } else {
    var nav = UINavigationController(rootViewController: vc)
    nav.modalPresentationStyle = UIModalPresentationStyle.Popover
    var popover = nav.popoverPresentationController
    popoverContent.preferredContentSize = CGSizeMake(500, 600)
    popover.delegate = self
    popover.sourceView = self.view
    popover.sourceRect = CGRectMake(100, 100, 0, 0)

    presentViewController(nav, animated: true, completion: nil)
  }
}

The naive approach has lots of issues:

How can we fix it?

Cleaning up your ViewControllers / ViewModels

This can be applied to MVVM, MVC and many other common patterns. When I talk about VC/VM, just think about the one you are using right now.

Let’s start by getting rid of all dependencies, instead of hardcoding related controllers, use Delegate or block based interface

class MyViewController {
  let onDone = (Void -> Void)?

  func doneButtonTapped() {
    onDone?(prepareNeccesaryState())
  }
}

ViewController / ViewModel should:

At this point we already improved testability, as we can now test if our interfaces are triggered without having side-effects, pseudo-code:

let vc = createVC()
var executed = false
vc.onDone = {
  executed = true
}
//! add code here to trigger done state
expect(executed).toEventually(beTruthy())

But how do we coordinate our app view controllers?

Introducing FlowController’s

A FlowController is a simple object that will manage part of your app, or as I like to think about it ‚a subset of use cases’.

Three main roles of FlowController are:

func configureProgramsViewController(viewController: ProgramsViewController, navigationController: UINavigationController) {
    viewController.state = state
    viewController.addProgram = { [weak self] barButton in
        guard let strongSelf = self else { return }
        let createVC = R.storyboard.createProgram.initialViewController!
        strongSelf.configureCreateProgramViewController(createVC, navigationController: navigationController)
        navigationController.pushViewController(createVC, animated: true)
    }
}

Common app architecture with FlowControllers looks like this:

We actually have ApplicationController that creates it, which is owned by AppDelegate, as a rule of thumb you should never reference your AppDelegate, ever.

If you app has some significant subset of user stories that can be thought as a whole and would require multiple screens (e.g. Creating new workout program) then you would create a new child FlowController for that part, and present it from your main flow controller.

Which means they can be re-used anywhere, if one step would be importing something from user photos framework, you can reuse that code in different part of app, e.g., EditProfile can use same picker to select user avatar.

The idea was originally introduced to me by Jim and Sami over a year ago, we have been using it constantly.

Even though our app changed drastically 3 times, our architecture took it with ease and we were able to reuse a lot of code, and quite a few controllers didn’t require any changes.

In my book the advantage of using an architecture like this are clear:

Now in some architectures there are similar concepts e.g. VIPER has router. But they are usually pretty complex and require a lot of up-front cost to adapt into existing app.

What’s great about this approach is the fact it’s straightforward to adapt to it right now, no need to wait for new project to try it. And it works in small and big projects just as well.

Doesn’t matter if you use MVVM, MVC or other pattern, if you are pushing your screens from one to the other, give it a try.


Share this post on:

Previous Post
Programming
Next Post
Little things that can make your life easier in 2016