Coordinator Design Pattern

Barış GÖRGÜN
adessoTurkey
Published in
6 min readSep 29, 2023

As iOS applications grow in complexity, it can become challenging to manage the navigation and communication between different components. The coordinator design pattern is a solution to this problem, allowing developers to organize their code and make it more modular. In this article, we will explore the coordinator design pattern in Swift and its benefits.

What Is the Coordinator Design Pattern?

The coordinator design pattern is a design pattern used to manage the navigation and communication between different components of an application. It separates the navigation code from the view controllers, making it easier to manage and maintain. The coordinator acts as a mediator between the view controllers and the navigation controller, handling the navigation logic and passing data between the different components.

Benefits of Using the Coordinator Design Pattern:

  1. The distinction between independence and responsibility: The coordinator is an independent component that manages each screen or module. This is ideal for managing different parts and screens of the application separately and testing each one individually.
  2. Facilitating transitions: The coordinator organizes transitions between screens and controls them from a central location. This simplifies complex transition logic and ensures stable management.
  3. Testability: Each coordinator has a single responsibility, making testing easier and improving debugging.

Let’s now move on to the code section and create our application together using the coordinator. The application I will create will consist of a total of 3 screens, and with the help of the coordinator, page transitions will occur when the buttons on the main screen are clicked.

Firstly, I won’t be using storyboards while developing this application, so let’s follow the steps below to remove the storyboard.

To remove the storyboard from the project, follow these steps:

  1. Delete the Main.storyboard file from the project navigator.
  2. Remove references to Main.storyboard from the project:
  • In the Info.plist file, remove the Main.storyboard file base name.
  • In the project settings, under the “General” tab, remove the reference to the Main.storyboard file in the “Main Interface” field.

3. Initialize the main window in code, since the application no longer has a storyboard set as the Main Interface.

Removing the storyboard allows for greater control, easier debugging, and easier scaling of the application. It also makes it easier for multiple developers to work on the project.

Delete the Main.storyboard file from the project navigator
Remove references to Main.storyboard from the project
In the Info.plist file, remove the Main storyboard file base name.

We can now start creating the coordinator pattern.

First, let’s define the coordinator protocol:

protocol Coordinator {
var navigationController: UINavigationController { get set }
func start()
}

The coordinator protocol is responsible for managing the navigation and communication between different components of the application. It declares a single method, start() that will be implemented by the coordinator classes to initiate the navigation flow.

Next, let’s create the MainCoordinator class that conforms to the coordinator protocol and handles the navigation:

class MainCoordinator: Coordinator {
var navigationController: UINavigationController

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

func start() {
let viewController = FirstScreenVC()
viewController.coordinator = self
navigationController.pushViewController(viewController, animated: false)
}

func showScreen2() {
let viewController = SecondScreenVC()
viewController.coordinator = self
navigationController.pushViewController(viewController, animated: true)
}

func showScreen3() {
let viewController = ThirdScreenVC()
viewController.coordinator = self
navigationController.pushViewController(viewController, animated: true)
}
}

In the above code, we have a start() method that initializes the first screen and sets the coordinator for that screen. The showScreen2() and showScreen3() methods are responsible for pushing the second and third screens onto the navigation stack, respectively.

Now, let's create the view controllers for each screen:

final class FirstScreenVC: UIViewController {

var coordinator: MainCoordinator?

private lazy var contentStack: UIStackView = {
let contentStack = UIStackView()
contentStack.distribution = .fillEqually
contentStack.axis = .vertical
contentStack.spacing = 20
contentStack.translatesAutoresizingMaskIntoConstraints = false
return contentStack
}()

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

}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setNavigationBar()
}

private func prepareUI() {
title = "Screen 1"
view.backgroundColor = .white

let secondVCButton = createButton(title: "Go to SecondVC", color: .systemBlue, action: #selector(goToSecondVC))
let thirdVCButton = createButton(title: "Go to ThirdVC", color: .systemGreen, action: #selector(goToThirdVC))

contentStack.addArrangedSubview(secondVCButton)
contentStack.addArrangedSubview(thirdVCButton)

view.addSubview(contentStack)
NSLayoutConstraint.activate([
contentStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
contentStack.heightAnchor.constraint(equalToConstant: 180),
contentStack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
contentStack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20)
])
}

private func setNavigationBar() {
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = .red
appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]

guard let navigationController = navigationController else { return }
navigationController.navigationBar.tintColor = .white
navigationController.navigationBar.standardAppearance = appearance
navigationController.navigationBar.compactAppearance = appearance
navigationController.navigationBar.scrollEdgeAppearance = appearance
}

private func createButton(title: String, color:UIColor, action: Selector) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = color
button.addTarget(self, action: action, for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}

@objc private func goToSecondVC() {
coordinator?.showScreen2()
}

@objc private func goToThirdVC() {
coordinator?.showScreen3()
}
}
final class SecondScreenVC: UIViewController {
var coordinator: MainCoordinator?

override func viewDidLoad() {
super.viewDidLoad()
title = "Screen 2"
view.backgroundColor = .systemBlue
}
}
final class ThirdScreenVC: UIViewController {
var coordinator: MainCoordinator?

override func viewDidLoad() {
super.viewDidLoad()
title = "Screen 3"
view.backgroundColor = .systemGreen
}
}

Each view controller has a coordinator property set by the coordinator when the view controller is initialized. This allows the view controller to communicate with the coordinator and trigger the navigation to the next screen.

Finally, you can configure the navigation controller in your application’s AppDelegate and SceneDelegate files and initialize the main coordinator.

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

let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = navigationController
window.makeKeyAndVisible()
self.window = window

mainCoordinator.start()
return true
}

This creates a new UIWindow instance and sets it as the root view controller of the navigation controller. This initializes the main coordinator with the navigation controller and makes it the navigation controller’s root view controller.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let navigationController = UINavigationController()

let mainCoordinator = MainCoordinator(navigationController: navigationController)
let window = UIWindow(windowScene: windowScene)
window.rootViewController = navigationController
window.makeKeyAndVisible()
self.window = window

mainCoordinator.start()
}

This creates a new UIWindow instance for the scene and sets its root view controller to the root view controller of the navigation controller in the AppDelegate.

When you run the application, we’ll be able to easily switch between pages as shown below.

Conclusion

The coordinator design pattern is a powerful tool for managing the navigation and communication between different components of your Swift application. By separating the navigation logic from the view controllers, you can make your code more modular, easier to test, and better organized. If you’re working on a complex iOS application, consider using the coordinator design pattern to simplify your codebase and improve its maintainability.

References :

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

--

--