Coordinator (coordonator) — un model arhitectural de navigare care mută logica tranzițiilor între ecrane din ViewController în clase separate. Modelul a fost propus de Soroush Khanlou în 2015 și a câștigat popularitate largă în comunitatea iOS. Coordinator gestionează flow-ul aplicației: creează și afișează ViewController, transmite date între ecrane și procesează finalizarea flow-ului. Modelul rezolvă problema Massive View Controller, eliminând navigarea din controler. Mai multe — în articolul original Coordinator.
Principalele puncte
Coordinator — un model care preia responsabilitatea navigării într-o aplicație iOS. În UIKit standard, ViewController gestionează singur tranzițiile: present, push, show segue — toate metodele de navigare sunt apelate din UIViewController. Coordinator mută această logică: ViewController raportează un eveniment (de exemplu, „utilizatorul a apăsat butonul de autentificare”), Coordinator decide ce ecran să afișeze în continuare. ViewController rămâne doar cu logica UI și delegă navigarea coordonatorului.
Structura modelului — CoordinatorProtocol cu metodele start() și finish(). start() — începutul flow-ului: crearea primului ViewController și afișarea. finish() — finalizarea flow-ului cu notificarea coordonatorului părinte. Router — o învelitoare peste UINavigationController (sau UISplitViewController) care oferă metodele show, push, pop, dismiss. Coordonatorul nu lucrează direct cu UINavigationController — doar prin Router. Acest lucru permite testarea navigării și schimbarea framework-ului UI.
| Componentă | Rol | Exemplu |
|---|---|---|
| Coordinator | Gestionarea flow-ului de navigare | AuthCoordinator, ProfileCoordinator |
| Router | Abstractizare peste UINavigationController | push, present, pop, dismiss |
| ViewController | UI + delegarea evenimentelor către Coordinator | LoginViewController.delegate |
Probleme pe care le rezolvă Coordinator — Massive View Controller (navigarea — o cauză frecventă a creșterii controlerului). În UIKit standard, ViewController conține prepareForSegue, delegați de navigare, procesarea unwind segues. Coordinator elimină acest lucru. Segue în storyboard — o legătură statică între ecrane, Coordinator oferă navigare dinamică cu condiții. Testarea navigării devine posibilă: se poate testa Coordinator fără UI, verificând secvența apelurilor Router.
Coordinator de bază în Swift — un protocol cu un tip asociat pentru Router și metodele start/finish. Router — un protocol care abstractizează UINavigationController. Implementarea concretă a Router învelește UINavigationController și îi delegă metodele. Coordinator primește Router în init și îl folosește pentru navigare. Coordonatorii copii sunt stocați în tabloul childCoordinators pentru gestionarea ciclului de viață.
// Router — abstractizare navigare
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 — gestionare 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)
}
}
Crearea Coordinator în AppDelegate/SceneDelegate — AppDelegate sau SceneDelegate creează UINavigationController, îl învelește în NavigationRouter, creează Coordinator-ul rădăcină (AppCoordinator) și apelează start(). AppCoordinator decide să afișeze onboarding, autentificare sau ecranul principal — în funcție de starea aplicației. Coordinator — singurul punct de intrare pentru navigare, ViewController nu știe despre alte ecrane.
Ierarhia coordonatorilor — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. Coordonatorul copil este creat de părinte și stocat în tabloul childCoordinators. Când coordonatorul copil își încheie activitatea, apelează finish() la părinte, iar părintele îl elimină din childCoordinators. Acest lucru previne scurgerile de memorie: Coordinator are o referință puternică la ViewController (prin Router), iar fără eliminarea din childCoordinators obiectul nu va fi eliberat.
// Delegat pentru comunicarea Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — părinte
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()
}
}
Gestionarea childCoordinators — eliminarea Coordinator-ului din tablou este singura modalitate de a-l elibera. Dacă uiți să elimini Coordinator-ul finalizat, acesta rămâne în memorie împreună cu ViewController-urile. Recomandat: didMove(toParent:) al părintelui, callback la finalizare sau publisher Combine pentru eliminare automată. Modelul Coordinator nu specifică mecanismul de notificare — delegat, closure sau Combine — alegerea aparține dezvoltatorului.
Transmiterea datelor prin delegat — Coordinatorul copil definește un protocol de delegat cu metode prin care se transmit rezultatele: func authCoordinator(_:didLoginWith user: User). Părintele implementează protocolul și primește datele la finalizarea flow-ului copil. Acest lucru este tip-sigur și explicit. Dezavantaj: pentru fiecare Coordinator copil trebuie scris un protocol separat. Pentru proiecte cu 10+ Coordonatori, aceasta duce la creșterea numărului de fișiere.
Transmiterea datelor prin tipul Result — metoda finish primește Result
| Metodă | Avantaje | Dezavantaje |
|---|---|---|
| Delegate | Tip-sigur, explicit, protocoale separate | Multe protocoale, mult boilerplate |
| Closure | Compact, mai puține fișiere | Dificil de depanat retain cycle |
| Combine/Rx | Reactiv, ușor de combinat | Dependență de bibliotecă, Debug mai dificil |
Strat de date partajat — Coordinatorii nu transmit date direct, ci folosesc un serviciu/repository comun. AuthCoordinator salvează token-ul în Keychain/UserDefaults, ProfileCoordinator citește de acolo. Coordinatorii comunică prin stare partajată (container Dependency Injection), nu prin apeluri directe. Aceasta reduce cuplarea Coordinatorilor, dar creează dependențe implicite de starea partajată.
Coordinator vs Router — Router este o componentă a Coordinatorului care abstractizează UINavigationController. Coordinator răspunde de flow (ce ecran să afișeze), Router — de mecanică (cum să afișeze: push/present). Router este „cum”, Coordinator este „ce”. Poți folosi Router fără Coordinator (de exemplu, Navigator-singleton), dar Coordinator fără Router — doar un ViewController cu o altă abstractizare. De obicei, ambele modele sunt folosite împreună.
Coordinator vs VIPER — VIPER are componenta Wireframe responsabilă de navigare — analogul Coordinatorului. În VIPER, Wireface face parte din modul, Coordinator — un strat separat deasupra modulelor. Modulul VIPER (View-Interactor-Presenter-Entity-Router) include navigarea ca parte a modulului. Coordinator este extern față de module: creează și conectează modulele, dar nu face parte din ele. Coordinator este mai flexibil pentru reutilizarea ecranelor în diferite flow-uri.
MVVM-C — extinderea MVVM cu Coordinator. ViewModel nu cunoaște direct Coordinator — ViewController delegă navigarea prin ViewModel, ViewModel apelează coordonatorul printr-un protocol. MVVM-C — abordarea standard pentru proiectele iOS cu SwiftUI: Coordinator gestionează NavigationStack sau fullScreenCover, ViewModel apelează coordonatorul prin publicarea stării. Apple nu recomandă Coordinator pentru SwiftUI — NavigationStack și NavigationPath sunt mecanisme de navigare încorporate.
// MVVM-C: ViewModel apelează Coordinator printr-un protocol
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// logică...
navigation?.showMainScreen()
}
}
Întrebări frecvente
Pentru SwiftUI, navigarea încorporată (NavigationStack, NavigationPath) înlocuiește adesea Coordinator. Apple recomandă navigarea bazată pe path. Coordinator are sens pentru flow-uri complexe cu condiții profunde (onboarding-autentificare-ecran principal în funcție de rol). Pentru aplicații simple pe SwiftUI, Coordinator este redundant — folosește NavigationPath.
Nu, sunt modele diferite. Coordinator gestionează flow-ul de navigare: decide ce ecran să afișeze, creează ViewController și le conectează. Router — abstractizare peste UINavigationController: push, present, pop, dismiss. Coordinator folosește Router pentru a executa navigarea. În unele implementări, Router include logica Coordinator (Router-per-screen), dar aceasta este o abatere de la modelul original.
Două locuri principale de scurgeri: childCoordinators (părintele ține copilul, uitând să-l elimine) și Router (UINavigationController ține ViewController). Soluție: elimină întotdeauna Coordinatorul copil din tablou la finish(). Folosește referință weak pentru delegate. Pentru Router — nu păstra o referință puternică la UINavigationController dacă este deja în ierarhia window. Testează deinit-ul Coordinatorului.
Pentru aplicații cu 3-5 ecrane, Coordinator este redundant — segue sau navigationController.pushViewController simplu sunt mai ușoare. Pentru aplicații SwiftUI cu NavigationStack — de asemenea redundant. Coordinator este justificat pentru aplicații cu 15+ ecrane, flow-uri complexe (onboarding cu ramificații, autentificare cu recuperare parolă) și proiecte mixte UIKit/SwiftUI.
Mock Router — verificarea ce metode sunt apelate și cu ce parametri. Verificarea childCoordinators: după start() tabloul nu este gol, după finish() — gol. Coordinator se testează fără UI: Router este un protocol, mock-ul său nu necesită UIKit. Folosește XCTestExpectation pentru flow asincron. În Android, analog — testarea NavigationController și NavHost cu mock-navigare.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și