Coordinator (συντονιστής) — ένα αρχιτεκτονικό πρότυπο πλοήγησης που μεταφέρει τη λογική των μεταβάσεων μεταξύ οθονών από το ViewController σε ξεχωριστές κλάσεις. Το πρότυπο προτάθηκε από τον Soroush Khanlou το 2015 και κέρδισε ευρεία διάδοση στην κοινότητα 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 αποφασίζει να εμφανίσει onboarding, σύνδεση ή κύρια οθόνη — ανάλογα με την κατάσταση της εφαρμογής. 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 publisher για αυτόματη αφαίρεση. Το πρότυπο Coordinator δεν προσδιορίζει τον μηχανισμό ειδοποίησης — delegate, closure ή Combine — η επιλογή είναι του προγραμματιστή.
Μεταφορά δεδομένων μέσω εκπροσώπου — ο παιδικός Coordinator ορίζει ένα πρωτόκολλο εκπροσώπου με μεθόδους μέσω των οποίων μεταφέρονται τα αποτελέσματα: func authCoordinator(_:didLoginWith user: User). Ο γονέας υλοποιεί το πρωτόκολλο και λαμβάνει δεδομένα κατά την ολοκλήρωση του παιδικού flow. Αυτό είναι ασφαλές τύπου και σαφές. Μειονέκτημα: για κάθε παιδικό Coordinator πρέπει να γραφτεί ξεχωριστό πρωτόκολλο. Για έργα με 10+ Coordinator, αυτό οδηγεί σε αύξηση του αριθμού αρχείων.
Μεταφορά δεδομένων μέσω τύπου Result — η μέθοδος finish δέχεται Result
| Μέθοδος | Πλεονεκτήματα | Μειονεκτήματα |
|---|---|---|
| Delegate | Ασφαλές τύπου, σαφές, ξεχωριστά πρωτόκολλα | Πολλά πρωτόκολλα, πολύ boilerplate |
| Closure | Συμπαγές, λιγότερα αρχεία | Δύσκολο να διορθωθεί retain cycle |
| Combine/Rx | Αντιδραστικό, εύκολος συνδυασμός | Εξάρτηση από βιβλιοθήκη, δυσκολότερο Debug |
Κοινόχρηστο επίπεδο δεδομένων — Οι Coordinator δεν μεταφέρουν δεδομένα άμεσα, αλλά χρησιμοποιούν κοινή υπηρεσία/αποθετήριο. Ο AuthCoordinator αποθηκεύει το token στο Keychain/UserDefaults, ο ProfileCoordinator διαβάζει από εκεί. Οι Coordinator επικοινωνούν μέσω κοινής κατάστασης (δοχείο Dependency Injection), όχι μέσω άμεσων κλήσεων. Αυτό μειώνει τη σύζευξη των Coordinator, αλλά δημιουργεί σιωπηρές εξαρτήσεις από την κοινή κατάσταση.
Coordinator vs Router — Ο Router είναι ένα στοιχείο του Coordinator που αφαιρεί το UINavigationController. Ο Coordinator είναι υπεύθυνος για το flow (ποια οθόνη να εμφανίσει), ο Router — για τη μηχανική (πώς να εμφανίσει: push/present). Ο Router είναι το „πώς“, ο Coordinator είναι το „τι“. Ο Router μπορεί να χρησιμοποιηθεί χωρίς Coordinator (π.χ., Navigator-singleton), αλλά ο 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 με βαθιές συνθήκες (onboarding-σύνδεση-κύρια οθόνη ανάλογα με τον ρόλο). Για απλές εφαρμογές 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(). Χρησιμοποιήστε weak αναφορά για το delegate. Για Router — μην κρατάτε ισχυρή αναφορά στο UINavigationController αν είναι ήδη στην ιεραρχία του window. Δοκιμάστε το deinit του Coordinator.
Για εφαρμογές με 3-5 οθόνες, ο Coordinator είναι περιττός — το segue ή το απλό navigationController.pushViewController είναι απλούστερο. Για εφαρμογές SwiftUI με NavigationStack — επίσης περιττός. Ο Coordinator δικαιολογείται για εφαρμογές με 15+ οθόνες, σύνθετα flow (onboarding με διακλαδώσεις, εξουσιοδότηση με επαναφορά κωδικού) και μεικτά έργα UIKit/SwiftUI.
Mock Router — έλεγχος ποιες μέθοδοι καλούνται και με ποιες παραμέτρους. Έλεγχος childCoordinators: μετά το start() ο πίνακας δεν είναι κενός, μετά το finish() — κενός. Ο Coordinator δοκιμάζεται χωρίς UI: το Router είναι πρωτόκολλο, το mock του δεν απαιτεί UIKit. Χρησιμοποιήστε XCTestExpectation για ασύγχρονο flow. Στο Android ανάλογα — δοκιμή του NavigationController και NavHost με mock-πλοήγηση.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης