Coordinator Pattern in Swift

Why do we need to change?

Recently, I realized that my app’s UI Layer instances are too much coupled.

After I searched why my code was not clean, I realized there are two reasons.

Let’s start by looking at code most iOS developers have written a hundred or more times:

if let vc = storyboard?.instantiateViewController(withIdentifier: "SomeVC") {
    navigationController?.pushViewController(vc, animated: true)
}

All ViewControllers have dependencies each other. Additionally, ViewModel also depend on their own controller. It makes developer to manage code hard later.

Therefore, I decided to remove all the navigating segues from viewcontrollers.

Because of initializing viewcontroller from viewcontroller, I should set the viewmodel in viewcontroller as well.

I need a Hub which manages navigation and Dependency injection.

It is Coordinator. Coordinator receives the request which ask navigating from current VC to the next VC. As a result, coordinator initiate the instance of VC, and show the screen.

Each VCs do not need to reference other VC. Furthermore, coordinator also supports dependency injection between ViewController and ViewModel.

As a result, All the coupled instance is seperated. Each instance classes do not need to manage other instances.

Coordinators in action

Coordinator Protocol

Protocol which abstracts implementation of coordinator. start() is neccessary for initial VC with storyboard.

Each coordinator can have children by the list of coordinator.

protocol Coordinator {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get set }

    func start()
}

Storyboarded

By adding protocol for each VC, it automatically supports initializing storyboard.

protocol Storyboarded {
    static func instantiate() -> Self
}

extension Storyboarded where Self: UIViewController {
    static func instantiate() -> Self {
        let className = String(describing: self)
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
        return storyboard.instantiateViewController(identifier: className) as! Self
    }
}

Main Coordinator

It’s a class rather than a struct because this coordinator will be shared across many view controllers.

It has an empty childCoordinators array to satisfy the requirement in the Coordinator protocol, but we won’t be using that here.

It also has a navigationController property as required by Coordinator, along with an initializer to set that property.

The start() method is the main part: it uses our instantiate() method to create an instance of our ViewController class, then pushes it onto the navigation controller.

class MainCoordinator: Coordinator {
    var childCoordinators = [Coordinator]()
    var navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let vc = ViewController.instantiate()
        vc.coordinator = self
        navigationController.pushViewController(vc, animated: false)
    }

    func buySubscription() {
        let vc = BuyViewController.instantiate()
        vc.coordinator = self
        navigationController.pushViewController(vc, animated: true)
    }

    func createAccount() {
        let vc = CreateAccountViewController.instantiate()
        vc.coordinator = self
        navigationController.pushViewController(vc, animated: true)
    }
}

AppDelegate

Set the rootViewController as NavigationController which can be controlled by coordinator.

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var coordinator: MainCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let navController = UINavigationController()

        coordinator = MainCoordinator(navigationController: navController)
        coordinator?.start()

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = navController
        window?.makeKeyAndVisible()

        return true
    }
}

ViewController

class ViewController: UIViewController, Storyboarded {
    weak var coordinator: MainCoordinator?

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func buyTapped(_ sender: Any) {
        coordinator?.buySubscription()
    }

    @IBAction func createAccount(_ sender: Any) {
        coordinator?.createAccount()
    }
}

Conclusion

I hope this has given you a useful introduction to the power of coordinators:

References

https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps

https://www.hackingwithswift.com/articles/175/advanced-coordinator-pattern-tutorial-ios