Coordinator Pattern
는 화면전환을 담당하는 패턴이다.
구현을 하다보면 화면 전환하는 로직도 MVC
에서는 C에 포함되어 있고, MVVM
에서도 V에 포함되어 있는 경우가 대부분이다.
하지만 이를 따로 분리해서 흐름을 제어할 수 있도록 한다면 각각 역할과 책임을 분리할 수 있어서 의존관계를 낮출 수 있다.
예를 들어 ViewController
에서 UI 작업과 비즈니스 로직을 처리해주는 MVC 패턴에서, 흐름을 제어하는 것은 UI 작업과 연관성이 떨어진다고 한다.
ViewController
에서 흐름관련 코드를 가지고 있다면 재사용성이 없어지며 오직 ViewController
에서만 쓸 수 있게 된다.
그래서 이를 해결하고자 Coordinator Pattern
이 나오게 되었다.
Coordinator
의 특징은 아래와 같다.
이 말은 그림을 보면 이해하기 쉽다.
구조는 보통 이런식으로 이루어져 있으며 가장 큰 Coordinator
가 존재하고 그 하위로는 이제 필요한화면의 Coordinator
가 하위로 존재한다.
그리고 이 하위 Coordinator
를 가지고 각 화면 전환을 담당할 수 있게 만든다.
실제 예시를 통해 보면 더 이해가 쉬울 것이다
실 예시는 아래 그림처럼 구현되어있다.
먼저 프로토콜로 AppCoordinator
를 만들어준다.
이곳에서는 Child
인 NavigationController
필요하고 start하는 메서드가 필요하다.
왜냐하면 시작하는 곧을 호출해야 하기 떄문이다!
protocol Coordinator: AnyObject {
var navigationController: UINavigationController { get set }
func start()
}
실제 구현은 아래와 같이 이루어진다 내부 메서드는 어떻게 동작하는지에 따라 구현이 달라질 것 같다.
이는 추후에 작성할 것이다 일단은 뼈대를 갖추도록 한다.
final class AppCoordinator: Coordinator {
var navigationController: UINavigationController
func start() {
}
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
private func showListView() {
}
func showDetailView() {
}
func dismiss() {
}
}
이렇게 사용하기 위해서는 SceneDelegate
에서 시작하는 화면을 AppCoordinator
로 시작해야한다
그래서 이에 대한 설정이 필요하다
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var coordinator: AppCoordinator?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
self.window = window
let navigationController = UINavigationController()
self.window?.rootViewController = navigationController
coordinator = AppCoordinator(navigationController: navigationController)
coordinator?.start()
self.window?.makeKeyAndVisible()
}
}
coordinator
변수를 가지고 있어야 하며, 네비게이션 컨트롤러를 AppCoordinator
에서 시작하도록 하고
만들어둔 start
메서드를 호출해서 화면 전환을 이곳에서 시작하도록 하면 된다.
이제 실제 Coordinator
를 구현해서 화면 전환을 어떻게 해야할지에 대한 로직이 필요하다
먼저 여기 예시에서는 ListView
를 보여주는 화면전환을 만드는 것이다.
화면에 TableView
인 ListView
가 보이도록 showListView
method를 만들었는데, 먼저 이곳에서는 데이터를 database
에 저장하고 있으므로 이를 가지고 있는 ListViewModel
를 생성한 다음, 실제 화면 전환하는 TodoListViewController
에 뷰모델을 넣은 것을 만든 다음 push
에서 사용한다.
그렇다면 원하는데이터가 들어있는 뷰모델을 통해 만든 뷰컨트롤러를 호출할 것이다
// AppCoordinator.swift
private let database = DatabaseManager()
func start() {
self.showListView()
}
private func showListView() {
let listViewModel = TodoListViewModel(dataBase: self.database)
let todoListViewController = TodoListViewController(
todoViewModel: listViewModel,
coordinator: self
)
self.navigationController.pushViewController(todoListViewController, animated: false)
}
이제 이를 실제 사용하는 곳은 아까 설정한 SceneDelegate
에서 했으므로 시작할 때 보여줄 것이다!
이제 2번째 화면으로 넘어가는 로직을 구현하도록 할 것이다.
이 코드에서는 파라미터로 Status
와 selectedTodo
를 받고 있다.
그 이유로는 TodoList
에서는 (Todo, Doing, Done)
3가지 Status
가 있으므로 이를 각각 확인할 로직이 필요하다.
또한 선택된 Todo
는 편집화면으로 들어가기 위해 선택된 셀이 무엇인지 알기 위해서 필요하다.
그래서 내부에서는 아까와 동일하게 detailViewModel
, detailViewController
를 만들면 되고
이를 보여주는 방식을 정한 다음 present
해주면 된다!
그리고 마지막에 dismiss
method를 통해 뒤로 돌아가는 것도 설정해줄 수 있다.
// AppCoordinator.swift
private var detailViewController: DetailViewController?
func showDetailView(todoListItemStatus: TodoListItemStatus? = .todo, selectedTodo: Todo? = nil) {
guard let todoListItemStatus = todoListItemStatus else {
return
}
let detailViewModel = DetailViewModel(database: self.database)
self.detailViewController = DetailViewController(
selectedTodo: selectedTodo,
todoListItemStatus: todoListItemStatus,
detailViewModel: detailViewModel,
coordinator: self
)
guard let detailViewController = detailViewController else {
return
}
let navigationController = UINavigationController(rootViewController: detailViewController)
navigationController.modalPresentationStyle = .formSheet
self.navigationController.present(navigationController, animated: false)
}
func dismiss() {
self.detailViewController?.dismiss(animated: true)
}
그리고 마지막으로 이를 실제 View
에서 어떻게 사용하는지 아래를 보면 확인할 수 있다.
// ListViewController.swift
private func bind() {
self.rightBarButton.rx.tap.asObservable()
.subscribe(onNext: { [weak self] in
self?.coordinator?.showDetailView()
})
.disposed(by: self.disposeBag)
}
첫 번째는 오른쪽 BarButton
눌렀을 경우 생성하는 로직으로 detailView
로 가게 하기위해서 사용하고 있다.
또한 위 로직에서 만들었던 셀을 눌렀을 경우 편집화면으로 가는 것은 이 코드로 확인할 수 있다.
rx에서 itemSelected
는 어떤 것을 눌렀는지 확인할 수 있으므로 이곳에서 누른 것을 바탕으로 coordinator
로 화면전환을 해줄 수 있다
// ListView.swift
self.tableView.rx.itemSelected
.subscribe(onNext: { [weak self] indexPath in
self?.listViewModel.cellSelectEvent(indexPathRow: indexPath.row, todoListItemStatus: self?.todoListItemstatus, completion: { [weak self] selectedTodo in
self?.coordinator?.showDetailView(todoListItemStatus: self?.todoListItemstatus, selectedTodo: selectedTodo)
})
})
.disposed(by: self.disposeBag)
참고사이트