티스토리 뷰
안녕하세요!
이번 글의 주제는 Coordinator Pattern이에요!
사실 iOS 관련 주제라고 할 수는 없지만.. 카테고리 선정이 애매해서 iOS에 포함시켰어요 :)
그래서 패턴 자체는 범용적으로 사용할 수 있지만 iOS 상황에서의 Coordinator Pattern에 대해 설명해보려고 해요 ☺️
왜 사용하는가?
앱 개발 상황을 가정해볼게요.
화면 전환은 매우 자주 일어나는 일인데 일반적으로 각 화면은 독립적인 ViewController를 가지고 있고 ViewController에서 다른 ViewController로 전환될 필요가 있어요.
이때 가장 단순한 방법은 하나의 ViewController의 인스턴스를 직접 생성해주며 화면을 전환시키는 거예요.
예시를 볼게요!
// in MainViewController
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedData = self.dataSource[indexPath]
let detailViewController = DetailViewController(payload: selectedData)
// Main 화면에서 세부적인 화면으로 이동
self.navigationController?.pushViewController(detailViewController, animated: true)
}
이렇게 되면 MainViewController가 DetailViewController의 인스턴스를 직접 생성하게 되고, 그 인스턴스를 소유하게 되기 때문에 두 화면 간의 의존성이 커져요.
Router라는 객체를 통해 화면 전환을 맡기면 이런 문제를 해결하는 동시에 Router의 역할은 화면 전환에만 한정되기 때문에 보다 좋은 구조라고 할 수 있어요.
Coordinator Pattern이란?
여기 이하의 설명들은 Raywenderlich를 포함해서, 대부분의 블로그 글들이 다루고 있는 내용이에요.
저는 일반적인 아래 설명들은 간략하게 넘어가고, 제가 이해한 Coordinator에 대한 생각을 설명해볼게요!
1. Coordinator 프로토콜
Coordinator는 구상체인 coordinator들이 반드시 구현해야 할 메서드와 프로퍼티를 정의해요. children 프로퍼티는 Coordinator Array이고 router는 Routing 객체예요. 이 두 프로퍼티의 관계를 정의하는 동시에 present와 dismiss를 정의해요.
즉, 각각의 Coordinator는 화면 presentation과 관련된 메서드들을 가지고 있어요.
public protocol Coordinator: class {
var children: [Coordinator] { get set }
var router: Router { get }
func present(animated: Bool, onDismissed: (() -> Void)?)
func dismiss(animated: Bool)
func presentChild(_ child: Coordinator,
animated: Bool,
onDismissed: (() -> Void)?)
}
2. Concrete Coordinator(Coordinator 구상체)
Coordinator 구상체는 당연히 Coordinator 프로토콜을 채택해요.
이 객체에서는 어떻게 다른 ViewController를 생성하는지, 그리고 이다음에 무슨 ViewController가 나와야 하는지 알고 있어요.
3. Router 프로토콜
Router 프로토콜은 당연한 이야기지만 Router 구상체들이 가져야 할 메서드들을 정의해요.
present, dismiss를 통해 다른 ViewController를 직접 보여주거나 없애는 기능을 가져요.
public protocol Router: class {
func present(_ viewController: UIViewController,
animated: Bool)
func present(_ viewController: UIViewController,
animated: Bool,
onDismissed: (()->Void)?)
func dismiss(animated: Bool)
}
extension Router {
public func present(_ viewController: UIViewController,
animated: Bool) {
present(viewController,
animated: animated,
onDismissed: nil)
}
}
4. Concrete Router(Router 구상체)
Router 구상체는 실제로 어떻게 ViewController를 present 할지 알고 있지만 어떤 ViewController가 다음에 나올지는 모르고 있어요. Coordinator가 router에게 어떤 ViewController를 불러야 하는지 알려주는 구조예요.
import UIKit
public class NavigationRouter: NSObject {
private let navigationController: UINavigationController
private let routerRootController: UIViewController?
private var onDismissForViewController:
[UIViewController: (() -> Void)] = [:]
public init(navigationController: UINavigationController) {
self.navigationController = navigationController
self.routerRootController =
navigationController.viewControllers.first
super.init()
}
}
extension NavigationRouter: Router {
public func present(_ viewController: UIViewController,
animated: Bool,
onDismissed: (() -> Void)?) {
onDismissForViewController[viewController] = onDismissed
navigationController.pushViewController(viewController,
animated: animated)
}
public func dismiss(animated: Bool) {
guard let routerRootController = routerRootController else {
navigationController.popToRootViewController(
animated: animated)
return
}
performOnDismissed(for: routerRootController)
navigationController.popToViewController(
routerRootController,
animated: animated)
}
private func performOnDismissed(for
viewController: UIViewController) {
guard let onDismiss =
onDismissForViewController[viewController] else {
return
}
onDismiss()
onDismissForViewController[viewController] = nil
}
}
5. Concrete ViewController
위의 구조라면 당연히 하나의 ViewController는 다른 ViewController의 존재에 대해 모르게 돼요.
그리고 화면 전환은 Coordinator에게 위임하는 구조예요.
😎 나름대로의 이해
UML이나 설명은 대략적으로 이해가 가지만 "그래서 어떻게 사용하나?"의 벽에 부딪히게 되었어요.
대부분의 글은 특정한 상황을 기반으로 설명되어 있는데, 제가 원하는 상황에는 적용되지 않았었어요.
저는
1. MVVM 패턴에서
2. RxSwift를 쓰는 상황에서
3. TabBarController로 시작하고
4. 각 TabBar에서 NavigationController를 갖는 구조
에서 Coordinator 패턴을 적용하고 싶었거든요.
그래서 제 나름대로 생각을 정리해봤어요.
🤔 Router는 반드시 필요한가?
기존에 VIP(a.k.a CleanSwift) 구조에서는 Coordinator 없이 Router 객체를 두어 화면을 전환했어요.
Factory를 통해 ViewController를 생성했기 때문에 ViewController 간 의존성이 높아질 걱정이 없는 구조였어요.
여기서도 당연히 Router 객체를 사용해야 한다는 고정관념을 가지고 여러 코드들을 살펴보았어요.
Router가 있는 경우에는 화면 전환 기능 자체는 Router가갖고, Coordinator가 화면 전환 요청을 관리하는 구조로 설계한다고 생각해요.
화면 전환의 필요성이 생기는 경우 Coordinator는 그 요청을 처리하고, Router라는 객체가 화면을 직접 전환함으로써 역할이 조금 더 세분화된다고 이해했어요.
반면 Router가 없는 경우는 Coordinator의 역할을 화면 전환으로 정의하고, Coordinator가 화면 전환 및 parent 및 child 관리 역할을 하는 것이죠. 큰 틀에서 이 두 역할 모두 "화면을 전환하는 역할"에 포함된다고 볼 수 있으니까요.
저는 제가 구현하는 프로젝트에서는 Router 객체가 따로 필요하다고 생각하지 않았어요.
만약 화면의 수가 많다면 Router의 책임을 분리함으로써 유지보수 및 가독성에서 유리한 측면이 커지겠지만, 현재 프로젝트에서는 구현할 화면 수가 많지 않아요.
그래서 화면 생성 및 전환을 Coordinator 객체가 갖는 구조로 설계했어요.
🙅🏻 다 같은 Coordinator가 아니에요
Coordinator를 이해할 때 가장 힘들었던 부분이에요.
대부분의 글들이 가장 단순한 화면을 구현하기 때문에 상황이 복잡하지 않아서 더 헷갈렸던 것 같아요.
제가 구현한 프로젝트의 구조는 아래와 같아요(글 하단에 PR 주소를 첨부할게요!).
TabBarViewController -> NavigationController -> ExchangeViewController -> CoinDetailViewController
Coordinator는 프로토콜로 기본적인 틀을 가지고 있어요.
말 그대로 기본적인 틀이고, 추상적인 역할만 정의되어 있기 때문에 실제로는 각 ViewController마다 Coordinator의 역할이 달라요.
1. TabBar의 Coordinator
앱 시작 시 RootViewController를 생성하기 위한 Coordinator에요.
트리 최상단의 Coordinator이기 때문에 parentCoordinator를 갖지 않아요.
window 프로퍼티 등 시작 화면 구성을 위한 요소를 갖고, TabBarController를 구현하기 위해 세부 화면 Coordinator들을 생성하여 childCoordinator에 등록하게 돼요.
이게 왜 필요할까를 생각해보았는데 한 번 생성된 Coordinator에서 화면 삭제 등 추가적인 작업이 필요할 때처럼 소유한 화면에 대한 관리를 위해 필요하다고 막연하게(?) 판단했어요.
2. NavigationController
NavigationController는 따로 Coordinator를 가질 필요가 없어요.
초기 생성 시 추가적인 세팅이 필요가 없고, rootViewController만 설정해주면 되기 때문이에요.
하지만 가장 중요한 것이 Navigation Stack을 관리하는 것인데, 이것을 위해서 중요한 것이 바로 다음이에요.
3. ExchangeViewController
이 화면은 Tab 선택 시 나오는 첫 화면인데, NavigationController의 RootViewController에요.
첫 화면이기 때문에 당연히 NavigationController가 생성되어 있지 않기 때문에 Navigation Controller를 생성해주어야 해요.
기존에는
NavigationViewController(rootViewController:)
의 방식으로 구현했는데 여기서는 프로퍼티로 가지고 있어요.
Coordinator의 프로퍼티로서 관리가 되는 이유는 다음 화면을 present 할 경우 이 Navigation Controller 자체를 통째로 넘겨주기 때문이에요.
이렇게 하지 않고 만약 모든 Coordinator에서 계속 Navigation Controller를 생성해준다면 당연히 Stack에서 push pop 할 수 있는 구조가 되지 않겠죠?
정리하면, 첫 화면의 Coordinator에서는 Navigation을 초기화하고 프로퍼티로 가지고 있는 상태로 화면을 전환할 때 이 프로퍼티를 함께 넘겨야 해요.
4. CoinDetailViewController(이후 모든 View)
두 번째 화면부터는 사실 세 번째, 네 번째 화면과 같은 구조가 될 거예요.
첫 화면과 다른 점이 한 가지 있는데, 프로퍼티 NavigationController의 상태예요.
// 3.ExchangeCoordinator
var navigationController: UINavigationController = .init()
// 4.CoinDetailCoordinator
var navigationController: UINavigationController
첫 화면은 NavigationController 자체가 존재하지 않기 때문에 Coordinator 자체에서 초기화를 시켜줘요.
반면 첫 화면과 달리 두 번째 화면은 반드시 Navigation Controller를 받아와야 하기 때문에 init을 통해 받아온 navigation controller로 프로퍼티를 초기화 해줘요.
그러면 새로운 화면을 몇 번 push 하든지 첫 번째 화면의 Navigation Stack에 쌓이게 되어서 관리가 가능해져요.
역할 정리
같은 Coordinator 프로토콜을 채택한다고 해도 전혀 다른 역할을 수행해요.
또한 가져야 하는 프로퍼티의 종류도 달라지기 때문에 각각의 위치에서의 역할을 이해할 수 있어야 해요.
결론
화면 전환의 요청이 들어올 경우 Coordinator는 화면을 생성해요.
단순히 생성만 하는 게 아니라 parent, child 프로퍼티를 통해 hierarchy를 관리해요.
start 메서드를 통해 해당 화면을 시작하며, navigation controller 안에서 생성할 경우 present가 아닌 stack에 push 하는 구조로 동작해요.
그래서 특정 화면 이후 띄울 화면 관리를 위한 배열(childCoordinators)과, 시작 시 화면을 띄워줄 메서드(start())를 가진 프로토콜로 만들어줬고, 그게 Coordinator 프로토콜이에요.
관련 PR
- [#23] 검색 결과 Cell을 클릭했을 때 Detail 화면이 나오도록 기반 작업을 해요
'iOS 앱개발 > iOS' 카테고리의 다른 글
[iOS] - SwiftUI 이해하기(1) (0) | 2022.05.29 |
---|---|
[iOS] ReactorKit 개념 이해하기(feat.README of ReactorKit) (0) | 2022.04.07 |
[iOS] RxTest를 통해 테스트하기 (0) | 2022.01.20 |
[iOS] ViewController 초기화시 init과 ViewDidLoad의 차이 (0) | 2022.01.19 |
[iOS] 시뮬레이터 없이 UI 확인하는 Preview 기능 만들기 (0) | 2022.01.03 |
- Total
- Today
- Yesterday
- NumberofDiscIntersections#Codility#Sort#Python
- django
- 터틀비치#리콘#xbox#controller
- 토마토#백준알고리즘#Python
- 파이썬알고리즘인터뷰#4장
- 나무자르기#BOJ#이분탐색#Python
- 쿼드트리#BOJ#분할정복#Python
- 섬의개수#백준알고리즘#Python
- Brackets#Stacks and Queues#Codility#Python
- 텀 프로젝트#백준알고리즘#Python
- PassingCars#Codility#Python
- 병든 나이트#BOJ#탐욕법#Python
- Triangle#Sorting#Codility#Python
- 랜선자르기#이분탐색#BOJ#Python
- Swift#Tuples#Range
- 암호코드#dp#BOJ#Python
- N으로 표현#DP#Programmers#Python
- Distinct#Codility#Python
- 공유기 설치#BOJ#이분탐색#Python
- django#slicing
- 반복수열#백준알고리즘#Python
- filter#isalnum#lower
- 날짜 계산#BOJ#완전탐색#Python
- API#lazy#
- 종이자르기#분할정복#BOJ#Python
- 배열합치기#분할정복#BOJ#Python
- 순열사이클#BOJ#Python
- 리모컨#완전탐색#BOJ#Python
- 백준 알고리즘#BackTracking
- 미로 탐색#백준알고리즘#Python
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |