Implementing Coordinator Concept with Modular in Swift

Naratpon Buakhaw
TakoDigital
Published in
4 min readOct 11, 2023

What is Coordinator?

แนวคิดที่ใช้ในการจัดการการนำทาง และการควบคุมการโต้ตอบระหว่างหน้าจอ หรือโมดูลต่าง ๆ ในแอปพลิเคชัน iOS โดยหน้าจอแต่ละหน้าจะไม่จำเป็นต้องรู้จักกัน และมี Coordinator ในบทบาทของผู้กำหนดทิศทางและผู้ควบคุมการเปลี่ยนแปลงหน้าจอ

Why do we need coordinator?

 let storyboard = UIStoryboard(name: "A2", bundle: bundle)
let viewController = storyboard.instantiateViewController(withIdentifier: "A2ViewController") as! A2ViewController
self.navigationController?.pushViewController(viewController, animated: true)

ลองมาดูตัวอย่างเพื่อให้เห็นภาพกัน

จากโค้ดด้านบนเราต้องการเรียกเพื่อเปลี่ยนหน้าไปยัง A2ViewController แน่นอนว่าเราจะต้องกำหนดค่าและคำสั่งการนำเสนอวิวจาก viewcontroller ของเราเพื่อระบุลิงค์จาก viewcontroller ของเราไปยัง A2ViewController แล้วถ้าเกิดว่า A2ViewController ยังจะสามารถถูกเรียกได้จากอีกหลายๆ viewcontroller ล่ะ แน่นอนว่าเราจะต้องทำซ้ำโค้ดแบบนี้ไปอีกเรื่อยๆใน viewcontroller นั้นๆ เพื่อกำหนดค่าและคำสั่งการนำเสนอวิวเปิด A2ViewController เห็นได้ชัดว่าจะเกิดการทำซ้ำของโค้ดไปเรื่อยๆ

ดังนั้นเราสามารถใช้ Coordinator เข้ามาช่วยให้เราแก้ปัญหาการทำโค้ดซ้ำและทำให้ให้แต่ละ viewcontroller ไม่จำเป็นต้องรู้จักกันและเป็นอิสระต่อกัน

เพื่อให้เห็นภาพ ข้อดีและประโยชน์ของ Coordinator มากขึ้นในบทความน้ีผมจะยกตัวอย่างการ implement เพื่อไว้ใช้สำหรับโปรเจคที่ขนาดใหญ่และสามารถมีโอกาส scale ใหญ่ขึ้นได้โดยในโปรเจคตัวอย่างผมจะทำในรูปแบบของ modular ไว้

ผมได้สร้างโปรเจคตัวอย่าง exmaple ไว้ในรูปแบบ modular มีโครงสร้างดังนี้

Application Structure

จากรูปจะแบ่ง layer ของ module ต่างๆไว้ดังนี้ โดยมี module ของ common อยู่ระดับต่ำสุดและ module feature ทั้งหมดอยู่ระดับเดียวกัน และแน่นอนว่า layer ชั้นล่างไม่สามารถมองเห็น layer ชั้นบนได้ ลองเล่นโปรเจคตัวอย่างได้ที่ Example project

Let’s Started

โดยในบทความนี้จะแนะนำ concept การ implement coordinatorโดยที่ต้องการให้แต่ละ viewcontroller มี builder เป็นของตัวเองเพื่อกำหนด identifier และ function ให้สำหรับ return viewcontroller และสามารถเรียก viewcontroller หากันได้เพียงแค่ กำหนดชื่อ scene

1. สร้าง enum transition type โดยประกอบด้วย case ต่างๆ เช่น root, push, present, pop, dismiss ซึ่งแต่ละ case จะมี parameter ที่ต้องการเช่น scene (ชื่อ scene identifier) และ animated (ตัวบอกว่าจะให้มี animation หรือไม่).

public enum ApplicationTransitionType {
case root(scene: String)
case push(scene: String, animated: Bool)
case present(scene: String, animated: Bool)
case pop(animated: Bool)
case dismiss(animated: Bool)
}

2. สร้าง protocol AppCoordinatorType โดย Protocol นี้กำหนดฟังก์ชัน navigate ซึ่งใช้ในการนำทางไปยัง scene ต่างๆ โดยรับ parameter เป็น enum ApplicationTransitionType.

public protocol AppCoordinatorType {
func navigate(_ type: ApplicationTransitionType)
}

3. สร้าง Protocol Builder และ SceneBuilderContainer

Protocol Builder กำหนดคุณสมบัติของ Builder ที่ใช้ในการสร้าง ViewController. ประกอบด้วย property sceneIdentifier เพื่อระบุ identifier ของ scene และฟังก์ชัน viewController สำหรับการสร้าง ViewController.

public protocol Builder {
var sceneIdentifier: String { get }
func viewController() -> UIViewController
}

Protocol SceneBuilderContainer กำหนดฟังก์ชันที่ใช้ในการ Build และ Assemble สำหรับ Dependency Injection. ประกอบด้วยฟังก์ชัน build ที่ใช้สร้าง Builder สำหรับ scene และ assemble ที่ใช้ในการ register Builder.

public protocol SceneBuilderContainer {
func build(for sceneIdentifier: String) -> Builder?
func assemble(builder: Builder, for sceneIdentifier: String)
}

4. สร้าง Singleton Class ที่สืบทอด Protocol SceneBuilderContainer และใช้ในการ build และ register. ส่วนตัวแปร builders ไว้เก็บ dictionary ของ Builders ที่สามารถใช้ในการ build ViewController

public class DependencyContainer: SceneBuilderContainer {
public init() {}

public static let shared = DependencyContainer()

private var builders: [String: Builder] = [:]

public func build(for sceneIdentifier: String) -> Builder? {
return builders[sceneIdentifier]
}

public func assemble(builder: Builder, for sceneIdentifier: String) {
builders[sceneIdentifier] = builder
}
}

5. สร้าง class AppCoordinator เพื่อใช้สำหรับกำหนดคุณสมบัติของ protocol AppCoordinatorType

public class AppCoordinator: AppCoordinatorType { }

สร้างฟังก์ชัน buildScene ที่ใช้สร้าง ViewController จาก Builder โดยให้รับ scene identifier

private func buildScene(scene: String) -> UIViewController {
guard let build = DependencyContainer.shared.build(for: scene) else {
fatalError("No builder found for scene identifier: \(scene)")
}
return build.viewController()
}

กำหนดฟังก์ชันใน case ต่างๆของการ navigate โดยเราจะเรียก function buildScene เพื่อหา viewcontroller ส่งไปเปิดหน้าต่างๆตาม case

public func navigate(_ type: ApplicationTransitionType){
switch type {
case .push(scene: let scene, animated: let animated):
let viewController = buildScene(scene: scene)
if let navigationController = UIViewController.topMostViewController()?.navigationController {
navigationController.pushViewController(viewController, animated: animated)
}
...........
......
...
}
}

6. สร้าง builder สำหรับ viewcontroller โดยจะกำหนด ชื่อ scene และ function return viewcontroller ตามคุณสมบัติ protocol Builder ที่เรากำหนดไว้ตอนแรก

public class A2ViewController: UIViewController {}
public struct A2Builder: Builder {
public init() {}

public var sceneIdentifier: String = "A2"

public func viewController() -> UIViewController {
let bundle = Bundle(identifier: "com.test.FeatureA")
let storyboard = UIStoryboard(name: "A2", bundle: bundle)
let viewController = storyboard.instantiateViewController(withIdentifier: "A2ViewController") as! A2ViewController
return viewController
}
}

7. สร้าง Class ที่ใช้ในการ Configure Dependency Container โดยการ register Builder ของแต่ละ scene.

class DependencyInjection {
func configure() {
let a1Builder = A1Builder()
let a2Builder = A2Builder()

DependencyContainer.shared.assemble(builder: a1Builder, for: a1Builder.sceneIdentifier)
DependencyContainer.shared.assemble(builder: a2Builder, for: a2Builder.sceneIdentifier)
}
}

8.เรียกคำสั่งเพื่อให้ App register scene และรู้จักกับ viewController ทั้งหมด

DependencyInjection().configure()

9. ตอนนี้ทุกอย่างพร้อมแล้ว เราสามารถเรียกฟังก์ชันที่ใช้ในการเรียก Coordinator เพื่อทำการ navigate ไปยัง scene “A2” โดยที่เราแค่ระบุชื่อ scene เท่านั้น

public class A1ViewController: UIViewController {
let coordinator = AppCoordinator()

public override func viewDidLoad() {
super.viewDidLoad()
}

@IBAction func tappedToA2(_ sender: Any) {
coordinator.navigate(.push(scene: "A2", animated: true))
}
}

หลังจากนี้ถ้าเรามีการสร้างหน้าใหม่ๆขึ้นมาเราก็แค่สร้างตัว builder และไป register viewcontroller ไว้ ทำให้ไม่ว่า viewcontroller ไหนที่ต้องการเรียกหากัน ก็แค่เพียงระบุ scene identifier เท่านั้น

coordinator.navigate(.push(scene: "identifier", animated: true))

Conclusion

คงจะพอมองภาพ concept ออกแล้ววิธีนี้จะช่วยเราได้เยอะมากๆไม่ว่าจะเป็นการลด massive code ส่วน navigate view เองก็ดีหรือ การที่ viewcontroller แต่ละตัวมีอิสระต่อกัน ที่ทุกๆ viewcontroller สามารถเรียกหากันได้โดยไม่มีข้อจำกัดเพราะว่ามันรับผิดชอบแค่ตัวของมันนั่นเอง

สุดท้ายนี้ถ้าเอาไปลองใช้งานจริงก็อาจจะต้องปรับกันอีกสักหน่อยยังไงก็ขอฝากแชร์ไว้เป็น idea ลองเล่นโปรเจคตัวอย่างดูได้ที่ Example project หวังว่าจะมีประโยชน์ ขอบคุณครับ..

Reference:

--

--