본문 바로가기

iOS/Pattern

iOS Coordinator 패턴 (개념 ~ 템플릿 적용 방법)

⛴ 개념


1️⃣ 서론

Coordinator 의미 : 움직임을 조정하는 사람

Coordinator Pattern은 2015년, Soroush Khanlou가 The Coordinator라는 글을 쓰면서 소개됩니다.

Khanlou는 ViewController가 flow로직, view로직, business로직등 너무 많은 역할을 한다고 생각하였습니다.

따라서 flow로직을 담당하는 객체를 만들었고 이 객체를 Coordinator 또는 Directors라고 지칭합니다.

💡 Coordinator패턴은 ViewController의 flow logic(흐름 로직)을 분리하기 위한 목적

 

2️⃣ 장점

  • 화면이 많아지게 되면, 화면전환을 담당하는 UINavigationController 를 사용하기가 버거워집니다. 왜냐하면 화면전환을 담당하는 코드가 ViewController에 의존하기 때문입니다. 따라서 Coordinator로 의존성을 분리하면 각각의 ViewController는 이전, 다음 ViewController에 대해서 알 필요가 없어지게 됩니다.
  1. 재사용성이 증가합니다.
  2. flow로직을 관리하기가 용이합니다.

 

🚀 사용방법


Coordinator패턴을 적용시키는 방법은 두가지로 나뉠 수 있습니다.

첫번째로 기본적인 방법으로는 자식Coordinator를 관리하지 않는 방법과 두번째로 자식Coordinator를 함께 관리하는 방법이 있습니다.

1️⃣ 자식 Coordinator를 사용하지 않는 방법

  1. Coordinator 프로토콜 생성
import UIKit

protocol Coordinator: AnyObject {
    var navigationController: UINavigationController { get set }

    init(navigationController: UINavigationController)

    func start()
}

 

2. 객체 생성하기

  • 많은 ViewController에서 공유되기 때문에 클래스로 작성합니다.
import UIKit

final class MainCoordinator: Coordinator {
    var navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ViewController")
        navigationController.pushViewController(vc, animated: false)
    }

}

 

3. 엔트리포인트에 Coordinator 연결하기

  • AppDelegate
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
		var window: UIWindow?
		var coordinator: MainCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
		    let navController = UINavigationController()
		    coordinator = MainCoordinator(navigationController: navController)
		    coordinator?.start()

		    window = UIWindow(frame: UIScreen.main.bounds)
		    window?.rootViewController = navController
		    window?.makeKeyAndVisible()

				return true
    }

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

}

 

  • SceneDelegate
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    var coordinator: MainCoordinator?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        let navController = UINavigationController()

        coordinator = MainCoordinator(navigationController: navController)
        coordinator?.start()

        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = navController
        window?.makeKeyAndVisible()
    }

}

 

 

4. 화면전환

  • 예시) ViewController → SecondViewController 추가
    1. ViewController에 Coordinator 프로퍼티 추가
    2. Coordinator 프로토콜에 화면전환 메서드선언
    3. MainCoordinator에 화면전환 코드 작성

2️⃣ 자식 Coordinator를 함께 사용하는 방법

💡 AppDelegate(SceneDelegate)는 최상위 AppCoordinator를 유지, 모든 Coordinator에는 일련의 하위Coordinator가 있습니다.

  1. Coordinator 프로토콜 생성
    • 자식Coordinator들을 관리할 배열을 선언합니다.
    // MARK: - 기본 Coordinator 프로토콜
    protocol Coordinator: AnyObject {
        var finishDelegate: CoordinatorFinishDelegate? { get set }
        var navigationController: UINavigationController { get set }
        var childCoordinators: [Coordinator] { get set }
        var type: CoordinatorType { get }
        func start()
        func finish()
        func findCoordinator(type: CoordinatorType) -> Coordinator?
    
        init(_ navigationController: UINavigationController)
    }
    
    extension Coordinator {
        func finish() {
            childCoordinators.removeAll()
            finishDelegate?.coordinatorDidFinish(childCoordinator: self)
        }
    
        func findCoordinator(type: CoordinatorType) -> Coordinator? {
            var stack: [Coordinator] = [self]
    
            while !stack.isEmpty {
                let currentCoordinator = stack.removeLast()
                if currentCoordinator.type == type {
                    return currentCoordinator
                }
                currentCoordinator.childCoordinators.forEach({ child in
                    stack.append(child)
                })
            }
            return nil
        }
    }
    
    
  2. 작업을 마친경우 작동할 프로토콜을 선언합니다.
protocol CoordinatorFinishDelegate: AnyObject {
    func coordinatorDidFinish(childCoordinator: Coordinator)
}

 

3. 각각의 Coordinator의 Type을 지정할 enum을 선언합니다.

enum CoordinatorType {
    case app, login, home
    case signUp, signIn
}

 

 

4. 각각의 Coordinator의 프로토콜을 선언합니다.

protocol LoginCoordinatorProtocol: Coordinator {
    func openSignUpCoordinator() // 회원가입 과정 시작
    func openSignInCoord() // 로그인 과정 시작
}

 

5. 객체(Coordinator)를 작성합니다.

final class LoginCoordinator: LoginCoordinatorProtocol {

    weak var finishDelegate: CoordinatorFinishDelegate?
    var navigationController: UINavigationController
    var loginViewController: LoginHomeViewController
    var childCoordinators: [Coordinator] = []
    var type: CoordinatorType = .login

    init(_ navigationController: UINavigationController) {
        self.navigationController = navigationController
        self.loginViewController = LoginHomeViewController.instantiate()
    }

    func start() {
        self.loginViewController.coordinator = self
        self.navigationController.viewControllers = [self.loginViewController]
    }

    func modalStart() {
        self.loginViewController.modalPresentationStyle = .fullScreen
        self.loginViewController.modalTransitionStyle = .crossDissolve
        self.navigationController.modalPresentationStyle = .fullScreen
        self.navigationController.modalTransitionStyle = .crossDissolve
        self.loginViewController.coordinator = self
        self.navigationController.viewControllers = [self.loginViewController]
    }

    func openSignUpCoordinator() {
        let signUpCoordinator = DefaultSignUpCoordinator(self.navigationController)
        signUpCoordinator.finishDelegate = self
        self.childCoordinators.append(signUpCoordinator)
        signUpCoordinator.start()
    }

    func openSignInCoord() {
        let signInCoordinator = DefaultSignInCoordinator(self.navigationController)
        signInCoordinator.finishDelegate = self
        self.childCoordinators.append(signInCoordinator)
        signInCoordinator.start()
    }

}

extension LoginCoordinator: CoordinatorFinishDelegate {
    func coordinatorDidFinish(childCoordinator: Coordinator) {
        self.childCoordinators.removeAll()
        self.finishDelegate?.coordinatorDidFinish(childCoordinator: self)
    }
}

 

템플릿 사용 및 규칙


📝 명명규칙

  1. 자식Coordinator의 프로토콜명은 [이름]CoordinatorProtocol 로 작성합니다.
    • ex) 로그인관련(LoginCoordinatorProtocol), 예약관련(ReservationCoordinatorProtocol)
  2. 위에서 만든 프로토콜을 준수하는 객체는 앞에 [이름]Coordinator 를 추가합니다.
    • ex) 로그인관련(LoginCoordinator), 예약관련(ReservationCoordinator)
  3. 화면 전환하는 메서드명은 전환방식에 따라 앞에 구분자를 추가합니다.
    1. navigation push로 전환하는 경우 앞에 pushTo~ 를 추가합니다.
      • ex) func pushToSignIn()
    2. modal 방식으로 전환하는 경우 앞에 present~ 를 추가합니다.
      • ex) func presentSignIn()
    3. 자식Coordinator를 추가하는 경우 앞에 open~ 를 추가한후 마지막에 자식Coordinator명도 작성합니다.
      • ex) func openSignInCoordinator()

👞 템플릿 작성

1️⃣ 프로젝트 템플릿

  • 프로젝트를 시작하는 경우 기본 AppCoordinator를 생성합니다.

2️⃣ 파일 템플릿

  • File → New 를 하여 파일명을 작성하면 위의 명명규칙에 따라서 파일이 자동으로 생성합니다.
  • Type과 tempViewController를 해당하는 ViewController들로 변경하여 사용합니다.

🔗 참고링크



'iOS > Pattern' 카테고리의 다른 글

Clean Architecture for iOS  (0) 2024.05.17