일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- swift concurrency
- Tuist
- DP
- 타임라인 포맷팅
- identifiable
- BFS
- paragraph style
- claen architecture
- SWIFT
- 버튼 피드백
- swift dashed line
- custombottomsheet
- RxSwift
- swift 점선
- task cancel
- uikit toast
- task cancellation
- swift navigationcontroller
- button configuration
- domain data
- swift bottomsheet
- reactorkit
- traits
- swift custom ui
- rxdatasources
- coordinator
- swift 백준
- custom navigation bar
- UIKit
- custom ui
- Today
- Total
김경록의 앱 개발 여정
[Swift] 코디네이터 패턴과 TabBar에서의 코디네이터 활용 본문
코디네이터 패턴(Coordinator Pattern)이란?
코디네이터 패턴의 개념
코디네이터 패턴은 앱에서 뷰 컨트롤러의 흐름을 관리하는 데 사용되는 설계 패턴입니다.
해당 패턴의 주요 목적은 뷰 컨트롤러 간의 전환과 상호작용을 뷰 컨트롤러 자체가 아닌 별도의 코디네이터 객체에서 담당하도록 하는 것 입니다.
코디네이터 패턴은 다음과 같은 방식으로 구현할 수 있습니다(다른 여러 방식 또한 존재합니다.)
import UIKit
public protocol Coordinator: AnyObject {
var parentCoordinator: Coordinator? { get set }
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
func didFinish()
}
public extension Coordinator {
func append(child: Coordinator) {
childCoordinators.append(child)
}
func remove(child: Coordinator) {
guard let targetIndex = childCoordinators.firstIndex(where: { $0 === child }) else {
return
}
childCoordinators.remove(at: targetIndex)
}
}
코디네이터 패턴의 장점
책임 분리
MVVM 패턴 기준에서 VC가 화면 이동의 책임까지 덜어낸다면 정말 View의 역할에 집중할 수 있게 됩니다.
재사용성
특정 흐름을 관리하는 코디네이터를 재사용할 수 있어 코드 중복을 줄이고 유지보수를 용이하게 합니다.
테스트 용이성
책임 분리로 인해 각 컴포넌트의 테스트가 용이해집니다. 코디네이터는 뷰 컨트롤러와 독립적으로 동작하므로 유닛 테스트가 더 쉽습니다.
코드 가독성
앱의 네비게이션 로직이 한 곳에 집중되므로 전체 앱의 흐름을 이해하기가 쉬워집니다. 이는 코드 리뷰와 유지보수를 더욱 용이하게 합니다.
Swift에서의 코디네이터 패턴
제가 사용한 코디네이터 패턴은 다음과 같습니다. 여러 구현 형태가 존재하지만, 자식 코디네이터를 관리하는 형태로 각 흐름을 자식 코디네이터로 관리할 수 있게끔 합니다.
해당 코디네이터 프로토콜을 채택하여 각 뷰가 가지는 코디네이터 구현체(Concrete)를 작성합니다.
구현체 예시입니다:
protocol TestViewCoordinator: Coordinator {
//추가적인 메서드가 정의됩니다. 예시로 TestViewController -> Test2ViewController를 모달로 띄우는 등의
//동작이 여기 작성될 수 있습니다.
}
final class DefaultTestViewCoordinator: TestViewCoordinator {
weak var parentCoordinator: Coordinator?
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
//이 코디네이터가 책임지는 뷰 컨트롤러를 참조합니다.
private var testViewController: TestViewController?
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
//MARK: Internal Methods
func start() {
testViewController = TestViewController()
testViewController?.coordinator = self
// 네비게이션을 통한 setViewController을 사용하든, 모달이든 푸쉬든 각자의 설계 방식으로 화면을 띄워주는 메서드를 포함
}
func didFinish() {
//부모 코디네이터에서 자신을 지웁니다.
parentCoordinator?.remove(child: self)
}
}
Start() 메서드는 많은 역할을 지닙니다.
추상화된 관점에서 코디네이터의 start만으로 해당 뷰 컨트롤러의 이동과 생성에 관한 모든 일을 처리해주면 됩니다.
기본적으로 각 뷰가 가진 coordinator를 자신으로 설정해주는 작업과 부모, 자식 관리가 포함되고 각 아키텍처에 따라 뷰 모델의 주입이나, UseCase의 주입 등이 이에 포함될 수 있습니다.
final class TestViewController: UIViewController {
weak var coordinator: TestCoordinator?
}
각 뷰는 코디네이터를 가집니다.
이때 구현체가 아닌 추상체에 의존하여 결합도를 낮춥니다.
weak 키워드를 사용하여 강한 순환 참조를 방지하는 것을 꼭 주의해야합니다.
AppDelegate에서의 코디네이터 사용
코디네이터 패턴을 적용하려면 AppDelegate에서 코디네이터를 초기화하고 앱의 첫 화면을 설정해야 합니다. 다음은 그 예입니다:
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var appCoordinator: AppCoordinator?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let navigationController = UINavigationController()
appCoordinator = AppCoordinator(navigationController: navigationController)
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
appCoordinator?.start()
return true
}
}
AppCoordinator는 전체 앱의 흐름을 관리하는 코디네이터입니다:
class AppCoordinator: Coordinator {
var parentCoordinator: Coordinator?
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let testCoordinator = DefaultTestViewCoordinator(navigationController: navigationController)
testCoordinator.parentCoordinator = self
append(child: testCoordinator)
testCoordinator.start()
}
func didFinish() {
parentCoordinator?.remove(child: self)
}
}
이렇게 하면 앱이 시작될 때 AppCoordinator가 초기화되고, 첫 화면을 설정하게 됩니다.
이를 통해 앱의 네비게이션 흐름이 코디네이터 패턴을 통해 관리될 수 있습니다.
TabBarController와의 조합
TabBarController에서 아이템을 추가할 때는 다음과 같은 메서드를 사용하게 됩니다:
TestTabBarController.viewControllers = []
하지만 코디네이터 패턴에서는 start() 메서드를 통해 뷰가 관리되는 느낌을 줘야합니다.
이를 위해 코디네이터의 start() 메서드가 UIViewController를 반환하도록 수정할 수 있습니다.
변경된 구현 방법
protocol TestViewCoordinator: Coordinator {
func start() -> UIViewController
}
final class DefaultTestViewCoordinator: TestViewCoordinator {
weak var parentCoordinator: Coordinator?
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
//이 코디네이터가 책임지는 뷰 컨트롤러를 참조합니다.
private var testViewController: TestViewController?
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() { }
func start() -> UIViewController {
testViewController = TestViewController()
testViewController?.coordinator = self
return testViewController!
}
func didFinish() {
parentCoordinator?.remove(child: self)
}
}
이렇게 하면 TabBarController에서 자연스럽게 코디네이터를 사용할 수 있습니다:
func start() {
let testCoordinator = DefaultTestViewCoordinator(navigationController: navigationController)
testCoordinator.parentCoordinator = self
append(child: testCoordinator)
let test2Coordinator = DefaultTest2ViewCoordinator(navigationController: navigationController)
test2Coordinator.parentCoordinator = self
append(child: test2Coordinator)
testTabBarController.viewControllers = [
testCoordinator.start(),
test2Coordinator.start()
]
}
이 방식은 start 메서드가 UIViewController를 반환하도록 함으로써, TabBarController의 viewControllers에 쉽게 추가할 수 있도록 합니다.
보통 tabBar가 메인화면을 차지하는 경우가 많을텐데, 앱코디의 자식으로 탭바 코디네이터가 자리잡으면 됩니다.
느낀점
사실 tabBar와 코디네이터패턴의 결합에 대해 오랜 시간 고민했습니다.
우리 팀이 사용하기로 한 코디네이터의 형태는 위와 같은데, 그 형태에 매몰되다 보니 도저히 생각이 뻗어나가지 못했습니다.
놓치고 있던 중요한 부분은 패턴의 고정된 형태를 억지로 유지하는것이 아닌 역할과 책임에 집중했어야했다는 점입니다.
"디자인 패턴은 정답이 없다" 라는 말이 이번 기회에 많이 와닿았던것 같습니다.
'TIL' 카테고리의 다른 글
[RxSwift] RxSwift에서 얘기하는 시퀀스 (0) | 2025.01.09 |
---|---|
[RxSwift] RxSwift (0) | 2025.01.09 |
[CS] 바이너리 파일이란? (0) | 2025.01.09 |
[Swift] 예약어를 변수로 사용하기 (백틱 ` `) (0) | 2025.01.09 |
[Swift]상수 파일(Constants) 관리하기 (0) | 2025.01.09 |