Coordinator (điều phối viên) — một mẫu kiến trúc điều hướng chuyển logic chuyển tiếp giữa các màn hình từ ViewController sang các lớp riêng biệt. Mẫu này được Soroush Khanlou đề xuất vào năm 2015 và đã được áp dụng rộng rãi trong cộng đồng iOS. Coordinator quản lý luồng ứng dụng: tạo và hiển thị ViewController, truyền dữ liệu giữa các màn hình và xử lý hoàn thành luồng. Mẫu này giải quyết vấn đề Massive View Controller bằng cách đưa điều hướng ra khỏi bộ điều khiển. Tìm hiểu thêm trong bài viết gốc về Coordinator.
Những điểm chính
Coordinator — một mẫu chịu trách nhiệm điều hướng trong ứng dụng iOS. Trong UIKit tiêu chuẩn, chính ViewController quản lý các chuyển tiếp: present, push, show segue — tất cả các phương thức điều hướng được gọi từ UIViewController. Coordinator trích xuất logic này: ViewController báo cáo một sự kiện (ví dụ: «người dùng đã nhấp vào nút đăng nhập»), Coordinator quyết định màn hình nào sẽ hiển thị tiếp theo. ViewController chỉ giữ lại logic UI và ủy quyền điều hướng cho coordinator.
Cấu trúc mẫu — CoordinatorProtocol với các phương thức start() và finish(). start() — bắt đầu luồng: tạo ViewController đầu tiên và hiển thị. finish() — kết thúc luồng với thông báo cho coordinator cha. Router — một lớp bọc quanh UINavigationController (hoặc UISplitViewController), cung cấp các phương thức show, push, pop, dismiss. Coordinator không làm việc trực tiếp với UINavigationController — chỉ thông qua Router. Điều này cho phép kiểm thử điều hướng và chuyển đổi framework UI.
| Thành phần | Vai trò | Ví dụ |
|---|---|---|
| Coordinator | Quản lý luồng điều hướng | AuthCoordinator, ProfileCoordinator |
| Router | Trừu tượng hóa trên UINavigationController | push, present, pop, dismiss |
| ViewController | UI + ủy quyền sự kiện cho Coordinator | LoginViewController.delegate |
Các vấn đề Coordinator giải quyết — Massive View Controller (điều hướng là nguyên nhân phổ biến gây phình to bộ điều khiển). Trong UIKit tiêu chuẩn, ViewController chứa prepareForSegue, các delegate điều hướng, xử lý unwind segues. Coordinator loại bỏ điều này. Segues trong storyboard là kết nối tĩnh giữa các màn hình, Coordinator cung cấp điều hướng động với các điều kiện. Việc kiểm thử điều hướng trở nên khả thi: có thể kiểm thử Coordinator mà không cần UI bằng cách xác minh trình tự các lệnh gọi Router.
Coordinator cơ bản trong Swift — một protocol với kiểu liên kết cho Router và các phương thức start/finish. Router — một protocol trừu tượng hóa UINavigationController. Một triển khai cụ thể của Router bao bọc UINavigationController và ủy quyền các phương thức cho nó. Coordinator nhận Router trong init và sử dụng nó để điều hướng. Các coordinator con được lưu trữ trong mảng childCoordinators để quản lý vòng đời.
// Router — trừu tượng hóa điều hướng
protocol RouterProtocol: AnyObject {
func push(_ viewController: UIViewController, animated: Bool)
func pop(animated: Bool)
func present(_ viewController: UIViewController, animated: Bool)
func dismiss(animated: Bool)
}
final class NavigationRouter: RouterProtocol {
private let navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func push(_ vc: UIViewController, animated: Bool) {
navigationController.pushViewController(vc, animated: animated)
}
func pop(animated: Bool) {
navigationController.popViewController(animated: animated)
}
func present(_ vc: UIViewController, animated: Bool) {
navigationController.present(vc, animated: animated)
}
func dismiss(animated: Bool) {
navigationController.dismiss(animated: animated)
}
}
// Coordinator — quản lý luồng
protocol CoordinatorProtocol: AnyObject {
var childCoordinators: [CoordinatorProtocol] { get set }
var router: RouterProtocol { get }
func start()
func finish()
}
class AuthCoordinator: CoordinatorProtocol {
var childCoordinators: [CoordinatorProtocol] = []
let router: RouterProtocol
init(router: RouterProtocol) {
self.router = router
}
func start() {
let loginVC = LoginViewController()
loginVC.onLogin = { [weak self] in
self?.showHome()
}
router.push(loginVC, animated: true)
}
private func showHome() {
let homeCoordinator = HomeCoordinator(router: router)
childCoordinators.append(homeCoordinator)
homeCoordinator.start()
}
func finish() {
childCoordinators.removeAll()
router.pop(animated: true)
}
}
Tạo Coordinator trong AppDelegate/SceneDelegate — AppDelegate hoặc SceneDelegate tạo UINavigationController, bọc nó trong NavigationRouter, tạo Coordinator gốc (AppCoordinator) và gọi start(). AppCoordinator quyết định hiển thị onboarding, đăng nhập hay màn hình chính — tùy thuộc vào trạng thái ứng dụng. Coordinator là điểm vào duy nhất cho điều hướng, ViewController không biết về các màn hình khác.
Hệ thống phân cấp coordinator — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. Coordinator con được tạo bởi cha mẹ và lưu trong mảng childCoordinators. Khi coordinator con hoàn thành công việc, nó gọi finish() trên cha mẹ, và cha mẹ xóa nó khỏi childCoordinators. Điều này ngăn rò rỉ bộ nhớ: Coordinator có tham chiếu mạnh đến ViewController (thông qua Router), và nếu không bị xóa khỏi childCoordinators, đối tượng sẽ không được giải phóng.
// Delegate cho giao tiếp Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — cha mẹ
class AppCoordinator: AuthCoordinatorDelegate {
func startAuthFlow() {
let authCoordinator = AuthCoordinator(router: router)
authCoordinator.delegate = self
childCoordinators.append(authCoordinator)
authCoordinator.start()
}
func authCoordinatorFinished(_ coordinator: AuthCoordinator) {
childCoordinators.removeAll { $0 is AuthCoordinator }
startMainFlow()
}
}
Quản lý childCoordinators — xóa Coordinator khỏi mảng là cách duy nhất để giải phóng nó. Nếu bạn quên xóa Coordinator đã hoàn thành, nó sẽ ở lại trong bộ nhớ cùng với các ViewControllers của nó. Các cách tiếp cận được khuyến nghị: didMove(toParent:) của cha mẹ, callback khi hoàn thành hoặc publisher Combine để tự động xóa. Mẫu Coordinator không chỉ định cơ chế thông báo — delegate, closure hay Combine — sự lựa chọn thuộc về nhà phát triển.
Truyền dữ liệu qua delegate — Coordinator con định nghĩa một protocol delegate với các phương thức để truyền kết quả: func authCoordinator(_:didLoginWith user: User). Cha mẹ triển khai protocol và nhận dữ liệu khi luồng con hoàn thành. Điều này an toàn về kiểu và rõ ràng. Nhược điểm: mỗi Coordinator con yêu cầu một protocol riêng. Đối với các dự án có 10+ Coordinator, điều này dẫn đến tăng số lượng tệp.
Truyền dữ liệu qua kiểu Result — phương thức finish chấp nhận Result
| Phương pháp | Ưu điểm | Nhược điểm |
|---|---|---|
| Delegate | An toàn kiểu, rõ ràng, protocol riêng | Nhiều protocol, nhiều boilerplate |
| Closure | Nhỏ gọn, ít tệp hơn | Khó gỡ lỗi retain cycle |
| Combine/Rx | Phản ứng, dễ kết hợp | Phụ thuộc thư viện, khó gỡ lỗi hơn |
Lớp dữ liệu chia sẻ — Các Coordinator không truyền dữ liệu trực tiếp mà sử dụng dịch vụ/kho lưu trữ chung. AuthCoordinator lưu token trong Keychain/UserDefaults, ProfileCoordinator đọc từ đó. Các Coordinator giao tiếp qua trạng thái chia sẻ (container Dependency Injection) thay vì gọi trực tiếp. Điều này giảm sự kết nối giữa các Coordinator nhưng tạo ra sự phụ thuộc ngầm vào trạng thái chia sẻ.
Coordinator vs Router — Router là thành phần của Coordinator trừu tượng hóa UINavigationController. Coordinator chịu trách nhiệm về luồng (màn hình nào sẽ hiển thị), Router — về cơ chế (cách hiển thị: push/present). Router là «cách thức», Coordinator là «cái gì». Có thể sử dụng Router mà không cần Coordinator (ví dụ: Navigator singleton), nhưng Coordinator không có Router chỉ là ViewController với một lớp trừu tượng khác. Thông thường cả hai mẫu được sử dụng cùng nhau.
Coordinator vs VIPER — VIPER có thành phần Wireframe chịu trách nhiệm điều hướng — tương tự Coordinator. Trong VIPER, Wireframe là một phần của module, Coordinator là một lớp riêng biệt phía trên các module. Một module VIPER (View-Interactor-Presenter-Entity-Router) bao gồm điều hướng như một phần của module. Coordinator là bên ngoài các module: nó tạo và kết nối các module nhưng không phải là một phần của chúng. Coordinator linh hoạt hơn cho việc tái sử dụng màn hình trong các luồng khác nhau.
MVVM-C — một mở rộng của MVVM với Coordinator. ViewModel không biết trực tiếp về Coordinator — ViewController ủy quyền điều hướng qua ViewModel, ViewModel gọi coordinator thông qua một protocol. MVVM-C là cách tiếp cận tiêu chuẩn cho các dự án iOS với SwiftUI: Coordinator quản lý NavigationStack hoặc fullScreenCover, ViewModel gọi coordinator bằng cách xuất bản trạng thái. Apple không khuyến nghị Coordinator cho SwiftUI — NavigationStack và NavigationPath là các cơ chế điều hướng tích hợp.
// MVVM-C: ViewModel gọi Coordinator qua protocol
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// logic...
navigation?.showMainScreen()
}
}
Các câu hỏi thường gặp
Đối với SwiftUI, điều hướng tích hợp (NavigationStack, NavigationPath) thường thay thế Coordinator. Apple khuyến nghị điều hướng dựa trên đường dẫn (path-based). Coordinator phù hợp cho các luồng phức tạp với điều kiện sâu (onboarding-đăng nhập-màn hình chính tùy theo vai trò). Đối với ứng dụng SwiftUI đơn giản, Coordinator là dư thừa — hãy sử dụng NavigationPath.
Không, đây là các mẫu khác nhau. Coordinator quản lý luồng điều hướng: quyết định màn hình nào sẽ hiển thị, tạo ViewControllers và kết nối chúng. Router là một lớp trừu tượng trên UINavigationController: push, present, pop, dismiss. Coordinator sử dụng Router để thực hiện điều hướng. Trong một số triển khai, Router bao gồm logic Coordinator (Router-per-screen), nhưng điều này đi chệch khỏi mẫu ban đầu.
Hai nguồn rò rỉ chính: childCoordinators (cha mẹ giữ con, quên xóa) và Router (UINavigationController giữ ViewController). Giải pháp: luôn xóa Coordinator con khỏi mảng khi finish(). Sử dụng tham chiếu weak cho delegate. Đối với Router — không giữ tham chiếu mạnh đến UINavigationController nếu nó đã có trong hệ thống phân cấp window. Kiểm thử deinit của Coordinator.
Đối với ứng dụng có 3-5 màn hình, Coordinator là dư thừa — segue hoặc navigationController.pushViewController đơn giản dễ dàng hơn. Đối với ứng dụng SwiftUI với NavigationStack — cũng dư thừa. Coordinator phù hợp cho ứng dụng có 15+ màn hình, luồng phức tạp (onboarding với rẽ nhánh, xác thực với khôi phục mật khẩu) và các dự án hỗn hợp UIKit/SwiftUI.
Mock Router — kiểm tra phương thức nào được gọi và với tham số nào. Kiểm tra childCoordinators: sau start() mảng không rỗng, sau finish() — rỗng. Coordinator được kiểm thử mà không cần UI: Router là một protocol, mock của nó không yêu cầu UIKit. Sử dụng XCTestExpectation cho luồng bất đồng bộ. Trên Android — kiểm thử tương tự NavigationController và NavHost với điều hướng mock.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm