OOZOU
Get in Touch
Back to Blog

Introduction to Coordinator

A brief introduction of iOS Coordinator pattern

June 9, 2019
8 min read
Introduction to Coordinator

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
        }
    }
}
lang-swift

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. 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
        }
    }
}
lang-swift

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
        }
    }
}
lang-swift

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
}
lang-swift

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.

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)
    }
}
lang-swift

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
  }
}
lang-swift

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.

Related Articles

How Can I Design a Software: A Beginners Guide for Business Owners

How Can I Design a Software: A Beginners Guide for Business Owners

Designing software is an exciting yet complex process that involves creativity, logical thinking, and problem-solving skills.

Read More →
The Future of Edge Computing: What Businesses Need to Know

The Future of Edge Computing: What Businesses Need to Know

The digital landscape is evolving rapidly, and businesses must adapt to keep up with rising demands for speed, efficiency, and real-time processing.

Read More →
The Future of Retail: AI, AR, and Digital Transformation Trends

The Future of Retail: AI, AR, and Digital Transformation Trends

Retail is evolving at an unprecedented pace, driven by AI (Artificial Intelligence), AR (Augmented Reality), and digital transformation technologies, revolutionizing the retail industry. Traditional brick-and-mortar stores are no longer just about selling products—they are becoming smart, interactiv

Read More →

Have a Project in Mind?

Let's discuss how we can help bring your ideas to life with our expertise in web and mobile development.

Start a Conversationhello@oozou.com
OOZOU Logo

Bangkok · Singapore · Hong Kong

X
Facebook
Instagram
LinkedIn

Services

  • Web Development
  • AI Agents & Generative AI
  • Mobile App Development
  • Data Analytics & Engineering
  • UI/UX & Product Design
  • Digital Transformation

Company

  • About Us
  • Careers
  • Blog
  • Case Studies
  • Today I Learned
  • Contact

More

  • Industries
  • Partner Network
© 2026 OOZOU. All rights reserved.
Privacy PolicyCode of ConductABAC Policy