iOS Design Patterns | #2 實作 Coordinator

黃暉德 Wade Huang
11 min readMay 8, 2024

--

前言

延續上一篇廣泛應用在 iOS 的 design patterns,最容易導入到專案中的應該就是 Coordinator,因此接下來就會來寫一款 App 來實作。

目錄

  • protocol coordinator
  • 建立 parent coordinator
  • 建立 child coordinators
  • 設定 scene delegate

Demo

先把之前畫的架構圖貼出來方便後續理解。

protocol Coordinator

首先,建立一個全新的 project,並新增一個 protocol Coordinator

import Foundation
import UIKit

protocol Coordinator: AnyObject {
// To push and pop views
var navigationController: UINavigationController { get set }
// Maybe it has child coordinators
var children: [Coordinator] { get set }

// Start the view controller
func start()
// Request events to decide the next view to present
func request(with event: CoordinatorEvent)
}

新增 enum CoordinatorEvent 來記錄事件和決定下一個畫面要顯示哪一個。

enum CoordinatorEvent {
case toLoginPage
case toVerifyPage
case toProductPage
case toPaymentPage
}

建立 parent coordinator

建立最上層的 parent coordinator,命名為 MainCoordinator 並遵從 Coordinator。在 start() 時會先將 AccountCoordinator append 到 children,但切換到 ShopCoordinator 職責的畫面時,children 就需要把前一個 AccountCoordinator 刪除,此時就需要遵從 UINavigationControllerDelegate 來呼叫 didShow..

class MainCoordinator: NSObject, Coordinator {
var navigationController: UINavigationController
var children: [Coordinator]

init(navigationController: UINavigationController) {
self.navigationController = navigationController
self.children = [Coordinator]()
}

func start() {
navigationController.delegate = self
startAccountCoordinator()
}

func request(with event: CoordinatorEvent) {
switch event {
case .toLoginPage, .toVerifyPage:
startAccountCoordinator()
case .toPaymentPage, .toProductPage:
startShopCoordinator()
}
}

private func startAccountCoordinator() {
let coordinator = AccountCoordinator(navigationController: navigationController)
children.append(coordinator)
coordinator.parentCoordinator = self
coordinator.start()
}

private func startShopCoordinator() {
let coordinator = ShopCoorinator(navigationController: navigationController)
children.append(coordinator)
coordinator.parentCoordinator = self
coordinator.start()
}

private func childCoordinatorDidFinish(_ coordinator: Coordinator) {
for (index, childCoordinator) in children.enumerated() {
if coordinator === childCoordinator {
children.remove(at: index)
break
}
}
}
}

extension MainCoordinator: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
// get from view controller when each view is presented
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else { return }

// pushing different view controller on top
if navigationController.viewControllers.contains(fromViewController) {
return
}

// popping that view is disappeared
if let verifyViewController = fromViewController as? VerifyViewController {
childCoordinatorDidFinish(verifyViewController.coordinator!)
}
}
}

建立 child coordinators

新增兩個 child coordinator,命名為 AccountCoordinator 和 ShopCoordinator,並將 parentCoordinator 設定為 MainCoordinator。
AccountCoordinator 處理 Login、Verify 的畫面。
ShopCoordinator 處理 Product、Payment 的畫面。

class AccountCoordinator: Coordinator {
weak var parentCoordinator: MainCoordinator?
var navigationController: UINavigationController
var children: [Coordinator]

init(navigationController: UINavigationController) {
self.navigationController = navigationController
self.children = [Coordinator]()
}

func start() {
request(with: .toLoginPage)
}

func request(with event: CoordinatorEvent) {
switch event {
case .toLoginPage:
let viewController = LoginViewController()
viewController.coordinator = self
navigationController.pushViewController(viewController, animated: true)
case .toVerifyPage:
let viewController = VerifyViewController()
viewController.coordinator = self
navigationController.pushViewController(viewController, animated: true)
default:
parentCoordinator?.request(with: .toProductPage)
}
}
}
class ShopCoorinator: Coordinator {
weak var parentCoordinator: MainCoordinator?
var navigationController: UINavigationController
var children: [Coordinator]

init(navigationController: UINavigationController) {
self.navigationController = navigationController
self.children = [Coordinator]()
}

func start() {
request(with: .toProductPage)
}

func request(with event: CoordinatorEvent) {
switch event {
case .toProductPage:
let viewController = ProductViewController()
viewController.coordinator = self
navigationController.pushViewController(viewController, animated: true)
case .toPaymentPage:
let viewController = PaymentViewController()
viewController.coordinator = self
navigationController.pushViewController(viewController, animated: true)
default:
parentCoordinator?.request(with: .toLoginPage)
}
}
}

設定 scene delegate

之後在 SceneDelegate 加入以下程式碼,讓 App 一執行就透過 MainCoordinator 接管畫面的處理,並呼叫 start() 顯示第一個畫面。

var coordinator: MainCoordinator?

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

let navigationController = UINavigationController()
coordinator = MainCoordinator(navigationController: navigationController)
coordinator?.start()

window = UIWindow(windowScene: windowScene)
window?.rootViewController = coordinator?.navigationController
window?.makeKeyAndVisible()
}

--

--