Coordinator (هماهنگکننده) — الگوی معماری ناوبری است که منطق انتقال بین صفحهها را از ViewController به کلاسهای جداگانه منتقل میکند. این الگو توسط Soroush Khanlou در سال ۲۰۱۵ ارائه شد و در جامعه iOS گسترش یافت. Coordinator جریان (flow) برنامه را مدیریت میکند: ViewController را ایجاد و نمایش میدهد، دادهها را بین صفحهها منتقل میکند و پایان flow را پردازش میکند. الگو مشکل Massive View Controller را با خارج کردن ناوبری از کنترلر حل میکند. بیشتر — در مقاله اصلی Coordinator.
نکات اصلی
Coordinator — الگویی که مسئولیت ناوبری در برنامه iOS را بر عهده میگیرد. در UIKit استاندارد، ViewController خودش انتقالها را مدیریت میکند: present، push، show segue — همه متدهای ناوبری از UIViewController فراخوانی میشوند. Coordinator این منطق را خارج میکند: ViewController درباره رویداد اطلاع میدهد (مثلاً «کاربر دکمه ورود را فشار داد»)، Coordinator تصمیم میگیرد کدام صفحه بعدی نمایش داده شود. ViewController فقط با منطق UI باقی میماند و ناوبری را به هماهنگکننده واگذار میکند.
ساختار الگو — CoordinatorProtocol با متدهای start() و finish(). start() — شروع flow: ایجاد اولین ViewController و نمایش آن. finish() — پایان flow با اعلان به هماهنگکننده والد. Router — پوششی بر UINavigationController (یا UISplitViewController) که متدهای show، push، pop، dismiss را ارائه میدهد. هماهنگکننده مستقیماً با UINavigationController کار نمیکند — فقط از طریق Router. این امکان تست ناوبری و تغییر فریمورک UI را فراهم میکند.
| مؤلفه | نقش | مثال |
|---|---|---|
| Coordinator | مدیریت flow ناوبری | AuthCoordinator, ProfileCoordinator |
| Router | انتزاع بر UINavigationController | push, present, pop, dismiss |
| ViewController | UI + واگذاری رویدادها به Coordinator | LoginViewController.delegate |
مشکلاتی که Coordinator حل میکند — Massive View Controller (ناوبری — دلیل رایج بزرگ شدن کنترلر). در UIKit استاندارد، ViewController شامل prepareForSegue، نمایندگان ناوبری، پردازش unwind segues است. Coordinator این را حذف میکند. Segue در storyboard — ارتباط ایستا بین صفحهها، Coordinator ناوبری پویا با شرایط را فراهم میکند. تست ناوبری ممکن میشود: میتوان Coordinator را بدون UI با بررسی توالی فراخوانیهای Router تست کرد.
Coordinator پایه در Swift — پروتکل با نوع وابسته برای Router و متدهای start/finish. Router — پروتکلی که UINavigationController را انتزاع میکند. پیادهسازی مشخص Router، UINavigationController را میپوشاند و متدها را به آن واگذار میکند. Coordinator Router را در init میپذیرد و برای ناوبری از آن استفاده میکند. هماهنگکنندههای فرزند در آرایه childCoordinators برای مدیریت چرخه حیات ذخیره میشوند.
// Router — انتزاع ناوبری
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 — مدیریت flow
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)
}
}
ایجاد Coordinator در AppDelegate/SceneDelegate — AppDelegate یا SceneDelegate UINavigationController ایجاد میکند، آن را در NavigationRouter میپیچد، Coordinator ریشه (AppCoordinator) ایجاد میکند و start() را فراخوانی میکند. AppCoordinator بسته به وضعیت برنامه تصمیم میگیرد آموزش، ورود یا صفحه اصلی را نشان دهد. Coordinator — تنها نقطه ورود برای ناوبری، ViewController از صفحههای دیگر اطلاعی ندارد.
سلسلهمراتب هماهنگکنندهها — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. هماهنگکننده فرزند توسط والد ایجاد میشود و در آرایه childCoordinators ذخیره میشود. وقتی هماهنگکننده فرزند کار خود را تمام میکند، finish() را در والد فراخوانی میکند و والد آن را از childCoordinators حذف میکند. این کار از نشت حافظه جلوگیری میکند: Coordinator ارجاع قوی به ViewController (از طریق Router) دارد و بدون حذف از childCoordinators، شیء آزاد نمیشود.
// نماینده برای ارتباط Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — والد
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()
}
}
مدیریت childCoordinators — حذف Coordinator از آرایه تنها راه آزادسازی آن است. اگر فراموش کنید Coordinator تمامشده را حذف کنید، همراه با ViewControllerها در حافظه باقی میماند. توصیه میشود: didMove(toParent:) والد، callback در پایان یا ناشر Combine برای حذف خودکار. الگوی Coordinator مکانیسم اعلان را مشخص نمیکند — نماینده، closure یا Combine — انتخاب با توسعهدهنده است.
انتقال داده از طریق نماینده — Coordinator فرزند پروتکل نماینده را با متدهایی که نتایج از طریق آنها منتقل میشوند تعریف میکند: func authCoordinator(_:didLoginWith user: User). والد پروتکل را پیادهسازی میکند و در پایان flow فرزند دادهها را دریافت میکند. این روش نوعاً ایمن و آشکار است. عیب: برای هر Coordinator فرزند باید پروتکل جداگانه نوشته شود. برای پروژههای با ۱۰+ Coordinator این باعث افزایش تعداد فایلها میشود.
انتقال داده از طریق نوع Result — متد finish Result
| روش | مزایا | معایب |
|---|---|---|
| Delegate | نوعاً ایمن، آشکار، پروتکلهای جداگانه | پروتکلهای زیاد، کد تکراری زیاد |
| Closure | فشرده، فایلهای کمتر | اشکالزدایی retain cycle دشوار است |
| Combine/Rx | واکنشی، ترکیب آسان | وابستگی به کتابخانه، Debug دشوارتر |
لایه داده مشترک — Coordinatorها داده را مستقیماً منتقل نمیکنند، بلکه از سرویس/مخزن مشترک استفاده میکنند. AuthCoordinator توکن را در Keychain/UserDefaults ذخیره میکند، ProfileCoordinator از آنجا میخواند. Coordinatorها از طریق وضعیت مشترک (ظرف تزریق وابستگی) ارتباط برقرار میکنند، نه از طریق فراخوانی مستقیم. این کار وابستگی Coordinatorها را کاهش میدهد، اما وابستگیهای ضمنی به وضعیت مشترک ایجاد میکند.
Coordinator vs Router — Router مؤلفه Coordinator است که UINavigationController را انتزاع میکند. Coordinator مسئول flow است (کدام صفحه نمایش داده شود)، Router — مسئول مکانیک (چگونه نمایش دهد: push/present). Router «چگونگی» است، Coordinator «چیستی». میتوان از Router بدون Coordinator استفاده کرد (مثلاً Navigator-تکتن)، اما Coordinator بدون Router — فقط ViewController با انتزاعی دیگر. معمولاً هر دو الگو با هم استفاده میشوند.
Coordinator vs VIPER — VIPER مؤلفه Wireframe را دارد که مسئول ناوبری است — مشابه Coordinator. در VIPER، Wireframe بخشی از ماژول است، Coordinator — لایه جداگانهای بالای ماژولها. ماژول VIPER (View-Interactor-Presenter-Entity-Router) ناوبری را به عنوان بخشی از ماژول شامل میشود. Coordinator نسبت به ماژولها خارجی است: ماژولها را ایجاد و متصل میکند، اما جزئی از آنها نیست. Coordinator برای استفاده مجدد از صفحهها در flowهای مختلف انعطافپذیرتر است.
MVVM-C — گسترش MVVM با Coordinator. ViewModel مستقیماً Coordinator را نمیشناسد — ViewController ناوبری را از طریق ViewModel واگذار میکند، ViewModel هماهنگکننده را از طریق پروتکل فراخوانی میکند. MVVM-C — رویکرد استاندارد برای پروژههای iOS با SwiftUI: Coordinator NavigationStack یا fullScreenCover را مدیریت میکند، ViewModel با انتشار وضعیت، هماهنگکننده را فراخوانی میکند. Apple Coordinator را برای SwiftUI توصیه نمیکند — NavigationStack و NavigationPath مکانیسمهای ناوبری داخلی هستند.
// MVVM-C: ViewModel از طریق پروتکل Coordinator را فراخوانی میکند
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// منطق...
navigation?.showMainScreen()
}
}
سؤالات متداول
برای SwiftUI ناوبری داخلی (NavigationStack, NavigationPath) اغلب جایگزین Coordinator میشود. Apple ناوبری مبتنی بر مسیر (path-based) را توصیه میکند. Coordinator برای flowهای پیچیده با شرایط عمیق (آموزش-ورود-صفحه اصلی بسته به نقش) معنا دارد. برای برنامههای ساده SwiftUI Coordinator اضافی است — از NavigationPath استفاده کنید.
خیر، این دو الگوی متفاوت هستند. Coordinator flow ناوبری را مدیریت میکند: تصمیم میگیرد کدام صفحه نمایش داده شود، ViewController ایجاد میکند و آنها را متصل میکند. Router — انتزاعی بر UINavigationController: push, present, pop, dismiss. Coordinator برای اجرای ناوبری از Router استفاده میکند. در برخی پیادهسازیها Router شامل منطق Coordinator است (Router-per-screen)، اما این انحراف از الگوی اصلی است.
دو مکان اصلی نشت: childCoordinators (والد فرزند را نگه میدارد و حذف را فراموش میکند) و Router (UINavigationController ViewController را نگه میدارد). راهحل: همیشه Coordinator فرزند را در finish() از آرایه حذف کنید. برای delegate از ارجاع weak استفاده کنید. برای Router — ارجاع قوی به UINavigationController نگه ندارید اگر در سلسلهمراتب window است. deinit Coordinator را تست کنید.
برای برنامههای با ۳-۵ صفحه Coordinator اضافی است — segue یا navigationController.pushViewController سادهتر است. برای برنامههای SwiftUI با NavigationStack نیز اضافی است. Coordinator برای برنامههای با ۱۵+ صفحه، flowهای پیچیده (آموزش با شاخهها، احراز هویت با بازیابی رمز عبور) و پروژههای ترکیبی UIKit/SwiftUI توجیهپذیر است.
Mock Router — بررسی کدام متدها با چه پارامترهایی فراخوانی میشوند. بررسی childCoordinators: بعد از start() آرایه خالی نیست، بعد از finish() — خالی است. Coordinator بدون UI تست میشود: Router یک پروتکل است، mock آن به UIKit نیاز ندارد. برای flow ناهمگام از XCTestExpectation استفاده کنید. در اندروید مشابه — تست NavigationController و NavHost با mock-ناوبری.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.