Coordinators in UIKit: An In-Depth Overview

Luiz Mello
3 min readAug 6, 2024

--

Coordinators are an integral part of UIKit, a framework that defines the core components and infrastructure for building graphical, event-driven applications in iOS. This article delves into the concept of Coordinators, discussing their importance, implementation, and providing code examples to illustrate their usage.

Importance of Coordinators

In UIKit development, managing the navigation flow and interactions between view controllers can become complex and cumbersome, especially in large applications. Coordinators address these challenges by encapsulating navigation logic, promoting a cleaner and more modular architecture. They decouple view controllers from navigation responsibilities, enhancing code readability, maintainability, and testability.

Origin and Concept of Coordinators

The Coordinator pattern originated as a way to manage view controllers in a scalable manner, especially in complex applications with deep navigation hierarchies. It follows the single responsibility principle by delegating the navigation logic to dedicated objects (coordinators) rather than having view controllers handle their own navigation.

Implementation of Coordinators

Implementing coordinators involves creating a Coordinator protocol, which declares required properties and methods. A base Coordinator class can handle common tasks, and individual coordinators inherit from the base class to manage specific flows. Below is a basic example of how to create a Coordinator:

Coordinator Protocol

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

func start()
}
  • childCoordinators: An array to keep track of all the child coordinators.
  • navigationController: The navigation controller used to present view controllers.
  • start(): A method each coordinator implements to set up its initial view controller.

Base Coordinator Class

Creating a base class for common functionality can simplify the implementation of specific coordinators.

class BaseCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController

init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
// To be overridden by subclasses
}
func addChildCoordinator(_ coordinator: Coordinator) {
childCoordinators.append(coordinator)
}
func removeChildCoordinator(_ coordinator: Coordinator) {
childCoordinators = childCoordinators.filter { $0 !== coordinator }
}
}

Concrete Coordinator Example

Let’s implement a concrete coordinator, for instance, a MainCoordinator:

class MainCoordinator: BaseCoordinator {
override func start() {
let vc = MyViewController()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}

func showDetail() {
let detailCoordinator = DetailCoordinator(navigationController: navigationController)
addChildCoordinator(detailCoordinator)
detailCoordinator.start()
}
}

In the MainCoordinator class:

  • It conforms to the Coordinator protocol by inheriting from BaseCoordinator.
  • It initializes with a navigationController.
  • In the start() method, it sets up the initial view controller and pushes it to the navigation stack.
  • The showDetail() method demonstrates how to start a child coordinator.

View Controller Integration

To integrate the coordinator with a view controller, you can add a coordinator property to the view controller.

class ViewController: UIViewController {
weak var coordinator: MainCoordinator?

override func viewDidLoad() {
super.viewDidLoad()
// Setup the view
}
@IBAction func showDetailTapped(_ sender: UIButton) {
coordinator?.showDetail()
}
}

Common Use Cases for Coordinators

  1. Onboarding Flows: Coordinators manage multi-step onboarding processes, ensuring a smooth transition between steps.
  2. Authentication: Handling login and signup flows, where different screens are coordinated based on user actions.
  3. Main App Flow: Coordinating the main navigation flow of the application, often involving tab bars and navigation controllers.
  4. Modular Feature Coordination: Managing navigation within specific features or modules of the app, improving modularity and code separation.

Example Use Case: Onboarding Flow

class OnboardingCoordinator: BaseCoordinator {
override func start() {
let onboardingVC = OnboardingViewController()
onboardingVC.coordinator = self
navigationController.pushViewController(onboardingVC, animated: true)
}

func showNextStep() {
let nextVC = NextStepViewController()
nextVC.coordinator = self
navigationController.pushViewController(nextVC, animated: true)
}
}

The implementation of the OnboardingCoordinator allows for organized and clear navigation between onboarding screens. This way, developers can easily add, remove, or modify onboarding steps without impacting the rest of the application’s code.

Moreover, by using coordinators, state management and presentation logic become simpler, as each coordinator is responsible for its own part of the navigation. This approach not only improves code readability but also facilitates testing, as the navigation logic can be isolated and tested independently.

Conclusion

Coordinators are a powerful design pattern in UIKit that help in managing the navigation flow of an app. They encourage a clean architecture by decoupling the view controllers from the navigation logic. By implementing a Coordinatorprotocol and creating a base class, we can create as many coordinators as needed to handle specific parts of the app, promoting modularity and maintainability. Embracing the Coordinator pattern can lead to more organized, scalable, and testable code in your iOS projects.

--

--

Luiz Mello
Luiz Mello

Written by Luiz Mello

iOS Engineer, passionate about programming, technology, design and coffee!

No responses yet