Introduction to Coordinator

A brief introduction of iOS Coordinator pattern

The Model-View-Controller (MVC) is a design pattern Apple chooses for iOS. I think it is a good choice because it is easy for newcomers to grasp and flexible enough for adaptation as your projects grow.

Once your projects grow in size and complexity, we try to find a solution and we end up with a new architecture (and a new problem), that’s why we see many new architectures coming out recently.

This post isn’t about new architecture, I just want to introduce you to a new piece of code with one specific task, control your application flow. I would say it just another C (controller) in MVC.

First, look at the following code. Most of you should familiar with it.

func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "showDetail" {
        if let indexPath = tableView.indexPathForSelectedRow {
        let object = fetchedResultsController.object(at: indexPath)
            let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
            controller.detailItem = object
            controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
            controller.navigationItem.leftItemsSupplementBackButton = true
        }
    }
}

This is a generated boilerplate when you create a new Master-Detail app project in Xcode.

What is the Problem?

Actually there is no obvious problem with this approach. If your app’s flow is simple and each view controllers only used once. You can get away with this approach. It serves me well for many years and still works in most of my projects.

You will realize some direct and indirect problems if your app has a complex flow with many screens in one flow.

Example: A shopping app

Before jumping to the problem let see some a sample, so we all get an idea of how complex flow I’m talking about. Actually it doesn’t need to be a rocket science app to see this problem.

Here we have a shopping app where users can make a purchase or physical products. As time goes by, we want to focus on digital products and ditch out all physical goods.


Here is our new flow, we removed 2 view controllers from our flow.

New flow


I won’t provide all classes of this sample project, but you can guess my naming and what does it do.

First, we skip 2 view controllers, so we have to change code in OrderSummaryViewController to the new destination.

From:

class OrderSummaryViewController: UIViewController {
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "checkout" {
            let vc = segue.destination as! DeliveryAddressViewController
            vc.cart = cart // pass along cart information to next view controller
        }
    }
}

To:

class OrderSummaryViewController: UIViewController {
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "checkout" {
            let vc = segue.destination as! ShippingOptionViewController
            vc.cart = cart // pass along cart information to next view controller
        }
    }
}

This might look like a simple change because both DeliveryAddressViewController and ShippingOptionViewController have the same interface in this case which is Cart. Cart keeps track of all data in the flow.

class Cart {
    var items: [Product]
    var paymentMethod: PaymentMethod
    
    // Old implementation
    var deliveryAddress: String
    var shippingOption: ShippingOption
}


When some object keeps track of “all data in the flow” this is a sign of code smell. Cart object contains too much information. All view controllers in the flow have to access this object which in fact they only need a subset of that information i.e. OrderSummaryViewController wants to know only products in a cart, DeliveryAddressViewController wants to know only addresses.

This inter-dependencies of view controllers make it hard to do proper dependency-injection, this left us with some ugly choice like global/shared state object like Cart or even singleton to pass along information to the end of the flow.

This global/shared state causes a problem when you work as a team since nobody want to touch this sacred Cart knowing that everyone is using it and changing it might affect others.

Problem

As you can see there are some problems with this design choice.

Tight coupling
Problems of this approach coming from the fact that a view controller knows too much about the next view controller. They are highly dependent on one another. A class assumes too many responsibilities and one concern is spread over many classes.

This leads to many more problems.

  • Reusability problem
When a class has many responsibilities and concerns, it hard to reuse this class elsewhere.

  • Hard to do dependency-injection
The one who does injection is the former view controller in the chain, this makes a view controller have an extra concern.

  • Global/Shared state
Everything that needed at the end of the flow needs to be pass along or everyone reference global state.

Maintenance problem
These inter-dependencies are not always known by other developers or even yourself (a few months later) and if there is a change in the flow (and you know it will be) it would be difficult to do so.

Looking for a new challenge? Join Our Team



Coordinator

The fact that we have to use Cart to remember all the things and pass along the flow implies that there is a missing piece of class that should responsible for this.

I think the Coordinator is a missing piece of this problem.

There are many Coordinator articles out there, you can search for a variety of Implementation and description. I will keep it simple and want you to think as a simple PONSO (Plain Old NSObject)/POSO (Plain Old Swift Object).

The idea of the Coordinator is to create a separate entity that is responsible for the application’s flow. In this case, I want this class to responsible for handle purchasing flow, so it will know the presentation flow and data needed in the flow.

// 1
protocol ProductListViewControllerDelegate: class {
    func productListViewController(_ vc: ProductListViewController, didSelectProduct product: Product)
}

class ProductListViewController: UIViewController {
    weak var delegate: ProductListViewControllerDelegate?
}

protocol ProductDetailViewControllerDelegate: class {
    func productDetailViewController(_ vc: ProductDetailViewController, didAddProduct product: Product)
}

class ProductDetailViewController: UIViewController {
    weak var delegate: ProductDetailViewControllerDelegate?
}

protocol OrderSummaryViewControllerDelegate: class {
    func orderSummaryViewControllerDidCheckout(_ vc: OrderSummaryViewController)
}

class OrderSummaryViewController: UIViewController {
    var items: [Cart.Item]!
    weak var delegate: OrderSummaryViewControllerDelegate?
}

// 2
struct Product {
    let id: String
    let name: String
    let price: Decimal
}

class Cart {
    struct Item {
        let product: Product
        var quantity: Int
    }
    
    var items = [Item]()
    
    func addProduct(_ product: Product) {}
    
    func removeProduct(_ product: Product) {}
}

// 3
class PurchaseCoordinator: ProductListViewControllerDelegate, ProductDetailViewControllerDelegate, OrderSummaryViewControllerDelegate {
    
    var cart = Cart()
    
    var paymentMethod: PaymentMethod?
    
    // Removed this in new implementation
    var deliveryAddress: String?
    var shippingOption: ShippingOption?
    
    var rootViewController: UINavigationController
    
    init() {
        let vc = ProductListViewController()
        rootViewController = UINavigationController(rootViewController: vc)
        
        vc.delegate = self
    }
    
    // MARK: - Product list
    // MARK: - Go to product detail
    func productListViewController(_ vc: ProductListViewController, didSelectProduct product: Product) {
        let vc = ProductDetailViewController()
        vc.delegate = self
        vc.navigationItem.rightBarButtonItem = checkoutButton
        
        rootViewController.pushViewController(vc, animated: true)
    }
    
    private var checkoutButton: UIBarButtonItem {
        return UIBarButtonItem(title: "Cart (\(cart.items.count))", style: .plain, target: self, action: #selector(didTapCheckout(_:)))
    }
    
    // MARK: - Go to order summary
    @objc func didTapCheckout(_ sender: UIBarButtonItem) {
        let vc = OrderSummaryViewController()
        vc.items = cart.items
        vc.delegate = self
        vc.navigationItem.leftBarButtonItem = closeButton
        
        rootViewController.present(UINavigationController(rootViewController: vc), animated: true, completion: nil)
    }
    
    // MARK: - Product detail
    // MARK: - Add product
    func productDetailViewController(_ vc: ProductDetailViewController, didAddProduct product: Product) {
        cart.addProduct(product)
    }
    
    // MARK: - Order Summary
    func orderSummaryViewControllerDidCheckout(_ vc: OrderSummaryViewController) {
        // Go to delivery or shipping options
    }
    
    
    // MARK: - Helpers
    private var closeButton: UIBarButtonItem {
        return UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapClose(_:)))
    }
    
    @objc func didTapClose(_ sender: UIBarButtonItem) {
        rootViewController.dismiss(animated: true, completion: nil)
    }
}


Quite a chunk of code here’s some breakdown.
  1. All view controllers are now focused on one job, the interface is clearer as you can see OrderSummaryViewController not accept Cart.Item, not Cart. View controllers communicate back to whoever interest by an old friend, delegate, nothing fancy.
  2. Cart also more reasonable, it only keeps a number of items in a cart. Everything else is moving to their own class ShippingOption and PaymentMethod and coordinator holding them.
  3. Coordinator holds all data necessary for the flow and controls the flow by present and dismiss view controllers.

You can simply use it like this.

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  private var purchaseCoordinator: PurchaseCoordinator
  
  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions:
                     [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
    let window = UIWindow(frame: UIScreen.main.bounds)
    let purchaseCoordinator = PurchaseCoordinator()
    
    self.window = window
    self.purchaseCoordinator = purchaseCoordinator
    
    window.rootViewController = purchaseCoordinator.rootViewController
    window.makeKeyAndVisible()

    return true
  }
}


Summary

I want to keep the implementation simple here, not using any protocol or anything fancy, but I think you can see some benefit of using a coordinator. Responsibility and concern of each class are more clear now, each class doing one job and delegate to others as soon as they can.

The coordinator might get massive overtime, but nothing preventing you from creating sub coordinators (where it make sense). Personally I don’t mind a line of code, if the flow is long I can live with a large coordinator with many lines of code as long as it does one job, coordinate the flow. You can come up with anything fancy here, but just using // MARK: is enough for me to keep code organized.

Ready to start your project? Contact Us

Like 260 likes
Art Wongpatcharapakorn
iOS Developer at Oozou
Share:

Join the conversation

This will be shown public
All comments are moderated

Get our stories delivered

From us to your inbox weekly.