Bài này được mình dịch từ đây

Ở bài trước, chúng ta đã biết sự phức tạp và rắc rối của controller, điều này giải thích vì sao MVC lại có nickname “Massive ViewController

Chúng ta hướng tới một ứng dụng với các mô-đun, dễ dàng bảo trì, khả năng mở rộng ứng dụng và nó thường đi kèm với vấn đề của nó. Một phần của vấn đề này có thể sửa đổi bằng cách bỏ kiến trúc MVC chuyển sang MVVM, với những công cụ trợ giúp.

Một trong số đó là luồng điều hướng phức tạp, không chỉ làm chậm quá trình phát triển của ứng dụng, mà còn làm cho code khó debug hơn.

May thay, là đã có Coordinator

Làm thế nào để ViewController phát triển thành quái vật?

Hãy xem đoạn code điều hướng tiêu chuẩn, như template của AppleMaster-Detail:

swift
1
2
3
4
5
6
7
8
9
10
11
extension MasterVC: UITableViewController {
	func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

		let detailData = getDetailData(indexPath: indexPath)

		let  detailVC = DetailVC()
		detailVC.detailData = detailData

		self.navigationController.pushViewController(detailVC, animated: true)
	}
}

Điều này có vẻ đơn giản, nhưng sẽ chứng minh sai vì một số lý do.

Navigation Controller là lớp cha của MasterVC, và lớp con ra lệnh cho nó. Trong trường hợp tốt nhất, lớp controller con không nên biết về lớp cha.

Chúng ta mong đợi nó sẽ hoạt động như nhau bất kể nó được trình diễn như là một phần của navigation stack, thêm một lớp ContrainerVC con hoặc hiển thị trong TabController.

Nó hoàn toàn tốt cho lớp ViewController con để ủy thác một số công việc, ngay cả khi nó ủy thác cho lớp cha. Tuy nhiên, khi lớp VC con có tham chiếu tới ViewController cha và trực tiếp gọi method của nó, toàn bộ điều đó trở nên có vấn đề.

Thực tế là một UIViewController có một tham chiếu đến UINavigationController trong ví dụ này vi phạm hệ phân cấp cha-con.

Ở đây chúng ta có thể thấy rằng MasterVC “biết” chính xác những gì đằng sau nó trong navigation stack, chuẩn bị dữ liệu và cấu hình viewcontroller tiếp theo. Không chỉ làm điều này gây ra gắn kết quá mức, mà nó còn đưa cho ViewController quá nhiều trách nhiệm. Cuối cùng, ViewController trong câu hỏi sẽ trở thành một con quái vật tuyệt đối rất khó để bảo trì và debug.

Một vấn đề khác là dữ liệu được hiển thị trong ViewController không phải là tập trung. Hãy tưởng tượng tình huống mà bạn có các stack sau đây:

Trong số những thứ khác, bạn cần phải hiển thị một tập con của dữ liệu từ VC1. Làm điều đó bằng cách nào? Bằng cách chuyển tiếp dữ liệu đó tới VC2, và sau đó đến VC3? Trong trường hợp đó, VC2 nhận và chuyển tiếp dữ liệu mà nó thậm chí không cần.

Và nếu bạn cần thêm một VC khác giữa VC2VC3 trong tương lai? Luồng này phát triển dài hơn và phức tạp hơn để debug, và đó không phải là điều tốt.

Trong navigation này, mọi ViewController đều biết phía sau nó, nhưng không có một object duy nhất phản ánh toàn bộ stack và có thể dễ dàng chuyển tiếp thông tin hoặc thêm, xóa ViewController khỏi stack.

Giết quái vật với Coordinator

Tất cả điều trên có thể dễ dàng sửa chữa với coordinator. Ngắn gọn, nó là object để điều khiển một hoặc nhiều ViewController.

coordinator chỉ là một NSObject đơn giản nên developer có toàn quyền kiểm soát nó. Coordinator có thể được khởi tạo bất cứ khi nào và bắt đầu khi nó phù hợp và nó không phụ thuộc vào vòng đời của một class hoặc lớp khác, giống như UIViewController.

Bây giờ, chúng ta hãy nói đến cấu trúc. Quy tắc của ngón tay cái là một coordinator được sử dụng cho một logical unit. Bằng cách đó, bạn phân chia ứng dụng thành các khối mô-đun, dễ quản lý.

Ví dụ, nếu bạn có luồng gửi ảnh tới máy chủ, coordinator sẽ phụ trách ViewController để chụp ảnh, thao tác ảnh và tải ảnh lên.

Coordinator cũng có thể được tổ chức trong hệ thống cấp bậc cha-con. Hệ phân cấp bắt đầu với root coordinator mà sau đó hiển thị một trong những coordinator con của nó, phụ thuộc vào trạng thái ứng dụng đang ở vào thời điểm nhất định.

Ví dụ phổ biến nhất là khi ứng dụng cần hiển thị luồng xác thực cho người dùng không đăng nhập hoặc màn hình chính cho người dùng.

Đây là cách chúng ta thường xử lý:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class RootCoordinator {
	func start() {
		if userLoggedIn() {
			showHomeCoordinator()
		} else {
			showAuthenticationCoordinator()
		}
	}

	func showHomeCoordinator() {
		let navigationController = UINavigationController()
		let homeCoordinator = HomeCoordinator(navigationController: navigationController)

		unowned(unsafe) let coordinator_ = homeCoordinator
		homeCoordinator.onEnding = { [weak self] in
			self?.removeChildCoordinator(childCoordinator: coordinator_)
		}

		addChildCoordinator(homeCoordinator)
		homeCoordinator.start()
	}

	func showAuthenticationCoordinator() {
		let navigationController = UINavigationController()
		let authenticationCoordinator = AuthenticationCoordinator(navigationController: navigationController)

		unowned(unsafe) let coordinator_ = authenticationCoordinator
		authenticationCoordinator.onEnding = { [weak self] in
			self?.removeChildCoordinator(childCoordinator: coordinator_)
		}

		addChildCoordinator(authenticationCoordinator)
		authenticationCoordinator.start()
	}
}

AuthenticationCoordinator là phụ trách tất cả các màn hình liên quan đến xác thực. Sau khi người dùng đã được chứng thực thành công, thông qua đăng nhập hoặc đăng ký, onEnding được thực hiện.

Điều này cũng có thể được thực hiện với một delegate, nhưng chúng ta muốn làm theo cách này để tránh mở rộng code và file, giữ mọi thứ trở nên sạch sẽ nhất có thể.

Root Coordinator

Tôi đã đề cập rằng coordinator là một NSObject đơn giản và có thể được sử dụng như trên. Tuy nhiên, có một số phương pháp nhất định lặp lại, vì vậy chúng ta đã viết một coordinator chung rằng tất cả các coordinator khác trong kế thừa ứng dụng:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Coordinator {

	// RootViewController for the Coordinator. This ViewController is injected into the Coordinator during the initialization and the Coordinator knows how to use it to display the ViewController it is in charge of.
	let rootViewController: UIViewController

	// List of Child Coordinators. Here is where all the references are stored for all Child Coordinators, so they are not deallocated before they need to be. The Parent Coordinator is in charge of deallocating all the Child Coordinators when the time is right.
	private(set) var childCoordinators: [Coordinator]

	// Block which the coordinator must call when it’s done its job. The Parent Coordinator usually deallocates the Child coordinator in this block.
	var onEnding: (() -> Void)?

	init(rootViewController: UIViewController) {
		self.rootViewController = rootViewController
		self.childCoordinators = []
    }

	// Function in charge of adding Child Coordinators
	func addChildCoordinator(childCoordinator: Coordinator) {
		childCoordinators.append(childCoordinator)
	}

	//Function in charge of removing a Child Coordinator
	func removeChildCoordinator(childCoordinator: Coordinator) {

		if let index = childCoordinators.index(of: childCoordinator) {
		childCoordinators.remove(at: index)
		} else {
		fatalError("Trying to remove not existing child coordinator")
		}
	}

	// Override this function to start the coordinator
	func start () {
	}
}

Và đây là cách chúng ta bắt đầu và khởi tạo root coordinator:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AppDelegate: UIResponder, UIApplicationDelegate {

		...

		private var rootCoordinator: RootCoordinator!

	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

		...
		let rootVC = UIViewController()
		window = UIWindow(frame: UIScreen.mainScreen().bounds)
		window!.rootViewController = rootVC
		window!.makeKeyAndVisible()

		let window = UIWindow(frame: UIScreen.main.bounds)

		rootCoordinator = RootCoordinator(rootViewController: rootVC)
		rootCoordinator.start()

		return true
	}

	...
}

Con quái vật VC bị thuần hóa

Cách tiếp cận này có một số lợi ích như:

  1. Không có sự kết hợp của nhiều ViewController. Một ViewController không cần biết bất cứ điều gì khác sau một hành động nhất định. Điều duy nhất cần làm là sử dụng coordinator bằng một hành động nhất định thông qua delegate hoặc closure.
  2. Mỗi ViewController có thể được tái sử dụng ở bất cứ đâu trong ứng dụng, và coordinator quyết định nó sẽ được hiển thị như thế nào (như là trong push navigation controller, tab controller hay bất kỳ cách nào khác).
  3. Nhiều controller có thể được nhóm lại trong một logical unit, cho phép chúng ta tái sử dụng toàn bộ thành phần.
  4. coordinator là một đối tượng thông thường, nhà phát triển có toàn quyền kiểm soát nó, và lần lượt, hoàn toàn kiểm soát luồng.

Cuối cùng, bạn kết thúc với một mô đun, dễ dàng quản lý code sẽ giúp bạn tiết kiệm thời gian bảo trì sau này. Và đó là mục tiêu từ đầu, đúng không?