Coordinator (ผู้ประสานงาน) — รูปแบบสถาปัตยกรรมการนำทางที่ย้ายตรรกะการเปลี่ยนระหว่างหน้าจอจาก ViewController ไปยังคลาสที่แยกต่างหาก รูปแบบนี้ถูกเสนอโดย Soroush Khanlou ในปี 2015 และได้รับการนำไปใช้อย่างกว้างขวางในชุมชน iOS Coordinator จัดการโฟลว์ของแอปพลิเคชัน: สร้างและแสดง ViewController ส่งข้อมูลระหว่างหน้าจอ และจัดการการสิ้นสุดโฟลว์ รูปแบบนี้แก้ปัญหา Massive View Controller โดยการนำการนำทางออกจากคอนโทรลเลอร์ เรียนรู้เพิ่มเติมใน บทความต้นฉบับเกี่ยวกับ Coordinator
ประเด็นสำคัญ
Coordinator — รูปแบบที่รับผิดชอบการนำทางในแอปพลิเคชัน iOS ใน UIKit มาตรฐาน ViewController จัดการการเปลี่ยนหน้าด้วยตนเอง: present, push, show segue — วิธีการนำทางทั้งหมดถูกเรียกจาก UIViewController Coordinator แยกตรรกะนี้ออกมา: ViewController รายงานเหตุการณ์ (เช่น «ผู้ใช้คลิกปุ่มเข้าสู่ระบบ») Coordinator ตัดสินใจว่าจะแสดงหน้าจอใดต่อไป ViewController เหลือเพียงตรรกะ UI และมอบหมายการนำทางให้กับผู้ประสานงาน
โครงสร้างรูปแบบ — CoordinatorProtocol พร้อมเมธอด start() และ finish() start() — จุดเริ่มต้นของโฟลว์: การสร้าง ViewController แรกและการแสดงผล finish() — การสิ้นสุดโฟลว์พร้อมการแจ้งเตือนไปยังผู้ประสานงานผู้ปกครอง Router — ตัวห่อหุ้มรอบ UINavigationController (หรือ UISplitViewController) ที่ให้เมธอด show, push, pop, dismiss ผู้ประสานงานไม่ได้ทำงานโดยตรงกับ UINavigationController — ผ่าน Router เท่านั้น ซึ่งช่วยให้สามารถทดสอบการนำทางและเปลี่ยนเฟรมเวิร์ก UI ได้
| คอมโพเนนต์ | บทบาท | ตัวอย่าง |
|---|---|---|
| Coordinator | การจัดการโฟลว์การนำทาง | AuthCoordinator, ProfileCoordinator |
| Router | นามธรรมเหนือ UINavigationController | push, present, pop, dismiss |
| ViewController | UI + การมอบหมายเหตุการณ์ให้ Coordinator | LoginViewController.delegate |
ปัญหาที่ Coordinator แก้ไข — Massive View Controller (การนำทางเป็นสาเหตุทั่วไปของการขยายตัวของคอนโทรลเลอร์) ใน UIKit มาตรฐาน ViewController มี prepareForSegue, ตัวแทนการนำทาง, การจัดการ unwind segues Coordinator กำจัดสิ่งเหล่านี้ Segues ใน 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 — การจัดการโฟลว์
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 ออบเจกต์จะไม่ถูกปล่อย
// Delegate สำหรับการสื่อสาร 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 ที่เสร็จสมบูรณ์ มันจะยังคงอยู่ในหน่วยความจำพร้อมกับ ViewControllers ของมัน วิธีการที่แนะนำ: didMove(toParent:) ของผู้ปกครอง, callback เมื่อเสร็จสิ้น หรือ Combine publisher สำหรับการลบอัตโนมัติ รูปแบบ Coordinator ไม่ได้ระบุกลไกการแจ้งเตือน — delegate, closure หรือ Combine — ตัวเลือกขึ้นอยู่กับนักพัฒนา
การส่งข้อมูลผ่าน delegate — Coordinator ลูกกำหนดโปรโตคอล delegate พร้อมเมธอดที่ใช้ส่งผลลัพธ์: func authCoordinator(_:didLoginWith user: User) ผู้ปกครองนำโปรโตคอลไปใช้และรับข้อมูลเมื่อโฟลว์ลูกเสร็จสมบูรณ์ วิธีนี้ปลอดภัยต่อชนิดและชัดเจน ข้อเสีย: Coordinator ลูกแต่ละตัวต้องมีโปรโตคอลแยกต่างหาก สำหรับโปรเจกต์ที่มี 10+ Coordinator จะทำให้จำนวนไฟล์เพิ่มขึ้น
การส่งข้อมูลผ่านชนิด Result — เมธอด finish รับ Result
| วิธีการ | ข้อดี | ข้อเสีย |
|---|---|---|
| Delegate | ปลอดภัยต่อชนิด, ชัดเจน, โปรโตคอลแยก | โปรโตคอลจำนวนมาก, โบยเลอร์เพลตมาก |
| Closure | กะทัดรัด, ไฟล์น้อยลง | ดีบัก retain cycle ยาก |
| Combine/Rx | รีแอกทีฟ, รวมกันง่าย | ขึ้นอยู่กับไลบรารี, ดีบักยากขึ้น |
เลเยอร์ข้อมูลที่ใช้ร่วมกัน — Coordinator ไม่ได้ส่งข้อมูลโดยตรง แต่ใช้บริการ/พื้นที่เก็บข้อมูลที่ใช้ร่วมกัน AuthCoordinator บันทึกโทเค็นใน Keychain/UserDefaults ProfileCoordinator อ่านจากที่นั่น Coordinator สื่อสารผ่านสถานะที่ใช้ร่วมกัน (คอนเทนเนอร์ Dependency Injection) แทนการเรียกโดยตรง ซึ่งลดการเชื่อมโยงระหว่าง Coordinator แต่สร้างการพึ่งพาโดยนัยต่อสถานะที่ใช้ร่วมกัน
Coordinator vs Router — Router เป็นคอมโพเนนต์ของ Coordinator ที่ทำให้ UINavigationController เป็นนามธรรม Coordinator รับผิดชอบโฟลว์ (จะแสดงหน้าจอใด) 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 ยืดหยุ่นกว่าสำหรับการใช้หน้าจอซ้ำในโฟลว์ต่าง ๆ
MVVM-C — ส่วนขยายของ MVVM ด้วย Coordinator ViewModel ไม่รู้จัก Coordinator โดยตรง — ViewController มอบหมายการนำทางผ่าน ViewModel ViewModel เรียก coordinator ผ่านโปรโตคอล MVVM-C เป็นแนวทางมาตรฐานสำหรับโปรเจกต์ iOS ด้วย SwiftUI: Coordinator จัดการ NavigationStack หรือ fullScreenCover ViewModel เรียก coordinator โดยการเผยแพร่สถานะ 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 เหมาะสมสำหรับโฟลว์ที่ซับซ้อนพร้อมเงื่อนไขลึก (การแนะนำ-เข้าสู่ระบบ-หน้าจอหลักขึ้นอยู่กับบทบาท) สำหรับแอปพลิเคชัน SwiftUI ง่าย ๆ Coordinator มากเกินไป — ใช้ NavigationPath
ไม่ใช่ ทั้งสองเป็นรูปแบบที่แตกต่างกัน Coordinator จัดการโฟลว์การนำทาง: ตัดสินใจว่าจะแสดงหน้าจอใด สร้าง ViewControllers และเชื่อมต่อ它们 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+ หน้าจอ โฟลว์ที่ซับซ้อน (การแนะนำแบบมีกิ่ง การยืนยันตัวตนพร้อมการกู้คืนรหัสผ่าน) และโปรเจกต์แบบผสม UIKit/SwiftUI
Mock Router — ตรวจสอบว่าเรียกเมธอดใดและด้วยพารามิเตอร์ใด ตรวจสอบ childCoordinators: หลัง start() อาร์เรย์ไม่ว่าง หลัง finish() — ว่าง Coordinator ถูกทดสอบโดยไม่ต้องใช้ UI: Router เป็นโปรโตคอล Mock ของมันไม่ต้องการ UIKit ใช้ XCTestExpectation สำหรับโฟลว์แบบอะซิงโครนัส ใน Android — การทดสอบที่คล้ายกันของ NavigationController และ NavHost ด้วยการนำทางแบบ mock
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม