Coordinator Pattern

Eddy📱·2022년 7월 24일
0

Design-Pattern

목록 보기
2/4
post-thumbnail

Coordinator Pattern는 화면전환을 담당하는 패턴이다.

구현을 하다보면 화면 전환하는 로직도 MVC에서는 C에 포함되어 있고, MVVM에서도 V에 포함되어 있는 경우가 대부분이다.

하지만 이를 따로 분리해서 흐름을 제어할 수 있도록 한다면 각각 역할과 책임을 분리할 수 있어서 의존관계를 낮출 수 있다.

예를 들어 ViewController에서 UI 작업과 비즈니스 로직을 처리해주는 MVC 패턴에서, 흐름을 제어하는 것은 UI 작업과 연관성이 떨어진다고 한다.

ViewController에서 흐름관련 코드를 가지고 있다면 재사용성이 없어지며 오직 ViewController에서만 쓸 수 있게 된다.

그래서 이를 해결하고자 Coordinator Pattern이 나오게 되었다.

Coordinator의 특징은 아래와 같다.

  • Coordinator에는 AppCoordinator가 존재한다.
  • 모든 Coordinator에는 하위의 Coordinator가 있다

이 말은 그림을 보면 이해하기 쉽다.

구조는 보통 이런식으로 이루어져 있으며 가장 큰 Coordinator가 존재하고 그 하위로는 이제 필요한화면의 Coordinator가 하위로 존재한다.

그리고 이 하위 Coordinator를 가지고 각 화면 전환을 담당할 수 있게 만든다.

실제 예시를 통해 보면 더 이해가 쉬울 것이다

실 예시는 아래 그림처럼 구현되어있다.

먼저 프로토콜로 AppCoordinator를 만들어준다.

이곳에서는 ChildNavigationController 필요하고 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를 보여주는 화면전환을 만드는 것이다.

화면에 TableViewListView가 보이도록 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번째 화면으로 넘어가는 로직을 구현하도록 할 것이다.

이 코드에서는 파라미터로 StatusselectedTodo를 받고 있다.

그 이유로는 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)

참고사이트

https://khanlou.com/2015/01/the-coordinator/

https://zeddios.medium.com/coordinator-pattern-bf4a1bc46930

profile
Make a better world

0개의 댓글