Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save Coder-ACJHP/e424afbb77077ec27cce02b4da09d573 to your computer and use it in GitHub Desktop.

Select an option

Save Coder-ACJHP/e424afbb77077ec27cce02b4da09d573 to your computer and use it in GitHub Desktop.
Coordinator Pattern in Swift — Decoupled Navigation with AppCoordinator & Child Coordinators
// MARK: - AppCoordinator
// Root coordinator that owns and manages all child coordinator flows.
// Acts as the single entry point for navigation in the app.
// Implements AuthCoordinatorDelegate to receive cross-flow navigation requests
// without tight coupling between child coordinators.
class AppCoordinator: AuthCoordinatorDelegate {
var navigationController: UINavigationController
// Dictionary-based storage allows O(1) lookup and safe cleanup by tag (UUID)
var childCoordinators: [String: any Coordinable] = [:]
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
@MainActor
func start() {
let authCoordinator = AuthCoordinator(navigationController: navigationController)
// When a child finishes, it removes itself from the registry to avoid memory leaks
authCoordinator.onFinish = { [weak self] tag in
self?.childCoordinators.removeValue(forKey: tag)
}
// AppCoordinator listens to AuthCoordinator for upward navigation requests
authCoordinator.delegate = self
childCoordinators[authCoordinator.tag] = authCoordinator
authCoordinator.startFlow()
}
@MainActor
func requestSettingsFlow() {
let settingsCoordinator = SettingsCoordinator(navigationController: navigationController)
settingsCoordinator.onFinish = { [weak self] tag in
self?.childCoordinators.removeValue(forKey: tag)
}
childCoordinators[settingsCoordinator.tag] = settingsCoordinator
settingsCoordinator.startFlow()
}
}
// MARK: - Coordinable Protocol
// Defines the contract every coordinator must fulfill.
// Enforces a consistent lifecycle: start → (navigate) → finish.
@MainActor
protocol Coordinable: AnyObject {
var tag: String { get set } // Unique identifier (UUID) for registry lookup
var onFinish: ((String) -> Void)? { get set } // Callback to notify parent when flow ends
var navigationController: UINavigationController { get }
func startFlow()
func finishFlow()
}
// MARK: - Cross-Flow Communication Protocol
// Allows a child coordinator (Auth) to request a sibling flow (Settings)
// without holding a direct reference to AppCoordinator.
// This breaks the tight coupling that would occur with direct parent references.
@MainActor
protocol AuthCoordinatorDelegate: AnyObject {
func requestSettingsFlow()
}
// MARK: - AuthCoordinator
// Owns the authentication flow: Login → OTP.
// Communicates upward via delegate (to AppCoordinator) for cross-flow transitions.
class AuthCoordinator: Coordinable {
var tag: String = UUID().uuidString
var onFinish: ((String) -> Void)?
var navigationController: UINavigationController
weak var delegate: AuthCoordinatorDelegate? // weak to avoid retain cycle with AppCoordinator
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func startFlow() {
let loginVC = LoginViewController()
loginVC.coordinator = self
self.navigationController.pushViewController(loginVC, animated: true)
}
func showOTP() {
let oTPViewController = OTPViewController()
oTPViewController.coordinator = self
self.navigationController.pushViewController(oTPViewController, animated: true)
}
// Delegates upward instead of directly instantiating SettingsCoordinator,
// keeping Auth flow isolated from Settings flow.
func showSettings() {
delegate?.requestSettingsFlow()
}
// Pops the current screen and signals AppCoordinator to release this coordinator
func finishFlow() {
self.navigationController.popViewController(animated: true)
onFinish?(tag)
}
}
// MARK: - ViewControllers (Auth Flow)
// Each VC holds a weak reference to its coordinator to avoid retain cycles.
// VCs only know about their immediate coordinator, never about AppCoordinator.
class LoginViewController: UIViewController {
weak var coordinator: AuthCoordinator?
@objc
func didTapToBack() {
coordinator?.finishFlow()
}
}
class OTPViewController: UIViewController {
weak var coordinator: AuthCoordinator?
@objc
func didTapToBack() {
navigationController?.popViewController(animated: true)
}
private func navigateToSettings() {
// Bubbles up through coordinator instead of navigating directly
coordinator?.showSettings()
}
}
// MARK: - SettingsCoordinator
// Owns the settings flow: Settings → Modal.
// Completely isolated from AuthCoordinator — launched by AppCoordinator on request.
class SettingsCoordinator: Coordinable {
var tag: String = UUID().uuidString
var onFinish: ((String) -> Void)?
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func startFlow() {
let settingsViewController = SettingsViewController()
settingsViewController.coordinator = self
self.navigationController.pushViewController(settingsViewController, animated: true)
}
func showModal() {
let modalVC = ModalViewController()
modalVC.modalTransitionStyle = .crossDissolve
modalVC.modalPresentationStyle = .formSheet
self.navigationController.present(modalVC, animated: true, completion: nil)
}
func finishFlow() {
self.navigationController.popViewController(animated: true)
onFinish?(tag)
}
}
// MARK: - ViewControllers (Settings Flow)
class SettingsViewController: UIViewController {
weak var coordinator: SettingsCoordinator?
@objc
func didTapToBack() {
coordinator?.finishFlow()
}
private func showModal() {
coordinator?.showModal()
}
}
// ModalViewController needs no coordinator reference:
// it is the terminal screen of the flow and only dismisses itself.
class ModalViewController: UIViewController {
@objc
func didTapToBack() {
self.dismiss(animated: true)
}
}
// MARK: - App Entry Point
// AppCoordinator is created once and retained for the app's lifetime.
// All navigation flows branch from this single root.
let navController = UINavigationController()
let coordinator = AppCoordinator(navigationController: navController)
coordinator.start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment