⛴ 개념
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에 대해서 알 필요가 없어지게 됩니다.
- 재사용성이 증가합니다.
- flow로직을 관리하기가 용이합니다.
🚀 사용방법
Coordinator패턴을 적용시키는 방법은 두가지로 나뉠 수 있습니다.
첫번째로 기본적인 방법으로는 자식Coordinator를 관리하지 않는 방법과 두번째로 자식Coordinator를 함께 관리하는 방법이 있습니다.
1️⃣ 자식 Coordinator를 사용하지 않는 방법
- 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 추가
- ViewController에 Coordinator 프로퍼티 추가
- Coordinator 프로토콜에 화면전환 메서드선언
- MainCoordinator에 화면전환 코드 작성
2️⃣ 자식 Coordinator를 함께 사용하는 방법
💡 AppDelegate(SceneDelegate)는 최상위 AppCoordinator를 유지, 모든 Coordinator에는 일련의 하위Coordinator가 있습니다.
- 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 } }
- 작업을 마친경우 작동할 프로토콜을 선언합니다.
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)
}
}
템플릿 사용 및 규칙
📝 명명규칙
- 자식Coordinator의 프로토콜명은 [이름]CoordinatorProtocol 로 작성합니다.
- ex) 로그인관련(LoginCoordinatorProtocol), 예약관련(ReservationCoordinatorProtocol)
- 위에서 만든 프로토콜을 준수하는 객체는 앞에 [이름]Coordinator 를 추가합니다.
- ex) 로그인관련(LoginCoordinator), 예약관련(ReservationCoordinator)
- 화면 전환하는 메서드명은 전환방식에 따라 앞에 구분자를 추가합니다.
- navigation push로 전환하는 경우 앞에 pushTo~ 를 추가합니다.
- ex) func pushToSignIn()
- modal 방식으로 전환하는 경우 앞에 present~ 를 추가합니다.
- ex) func presentSignIn()
- 자식Coordinator를 추가하는 경우 앞에 open~ 를 추가한후 마지막에 자식Coordinator명도 작성합니다.
- ex) func openSignInCoordinator()
- navigation push로 전환하는 경우 앞에 pushTo~ 를 추가합니다.
👞 템플릿 작성
- 프로젝트를 시작하거나 파일을 만드는 경우 조금 더 상용적인 코드들을 미리 작성해주는 템플릿을 사용할 수 있습니다.
- https://github.com/sookim-1/CoordinatorPatternXcodeTemplate 저장소로 이동하여 템플릿을 추가합니다.
1️⃣ 프로젝트 템플릿
- 프로젝트를 시작하는 경우 기본 AppCoordinator를 생성합니다.
2️⃣ 파일 템플릿
- File → New 를 하여 파일명을 작성하면 위의 명명규칙에 따라서 파일이 자동으로 생성합니다.
- Type과 tempViewController를 해당하는 ViewController들로 변경하여 사용합니다.
🔗 참고링크
- HackingWithSwift
- Khanlou
- 메이트러너 블로그 - Coordinator 적용기
- Zedd 블로그 - Coordinator패턴
- velog - ellyheetov
'iOS > Pattern' 카테고리의 다른 글
Clean Architecture for iOS (0) | 2024.05.17 |
---|