관리 메뉴

김종권의 iOS 앱 개발 알아가기

[Clean Architecture] 6. 코드로 알아보는 SOLID - Coordinator 패턴 화면전환 본문

Clean Architecture/Clean Architecture 코드

[Clean Architecture] 6. 코드로 알아보는 SOLID - Coordinator 패턴 화면전환

jake-kim 2021. 9. 21. 14:15

 

0. 코드로 알아보는 SOLID - 클래스 다이어그램 필수 표현

1. 코드로 알아보는 SOLID - SRP(Single Responsibility Principle) 단일 책임 원칙

2. 코드로 알아보는 SOLID - OCP(Open Close Principle) 개방 폐쇄 원칙

3. 코드로 알아보는 SOLID - LSP(Liskov Substitution Principle) 리스코프 치환 원칙

4. 코드로 알아보는 SOLID - ISP(Interface Segregation Principle) 인터페이스 분리 원칙

5. 코드로 알아보는 SOLID - DIP(Dependency Inversion Principle, testable) 의존성 역전 원칙

6. 코드로 알아보는 SOLID - Coordinator 패턴 화면전환

7. 코드로 알아보는 SOLID - Network, REST (URLSession, URLRequest, URLSessionDataTask)

8. 코드로 알아보는 SOLID - 캐싱 Disk Cache (UserDefeaults, CoreData)

Coordinator 패턴

  • 화면전환하는 기능을 분리
  • Coordinator를 사용하지 않은 경우 A -> B -> C 화면전환 시, A -> C로 화면전환 하려면, A에 코드가 새로 생성되어야 하지만 Coordinator를 사용하면 기존에 C로 이동하는 코드가 작성되어 있으므로 그대로 사용

Coordinator 사용

  • ViewModel에서 dependencies로 가지고 있는 ViewModelAction을 통해 화면전화하는데, ViewModelAction은 Coordinator에서 구현하여 주입하는 형태
  • ViewModel은 Action을 사용함으로써 화면전환 코드에 의존하지 않는 형태

  • DIContainer - Coordinator - ViewModel 화면 전환을 구현부터 사용하기까지

코드에서 Coordinator 구현 방법

  • 테크닉: 구현된 클로저를 파라미터로 넘겨서, 인수는 사용하는 곳에서 넣어줌으로서 `구현`하는  부분과 `인수`를 넣어주는 부분을 다르게 하는 방법
  • 화면전환시 필요한 ViewController 생성은 DIContainer에서 정의, ViewModel의 Action에 따라 화면 전환 로직은 Coordinator에서 정의, Action은 ViewModel에서 호출

Coordinator와 Action

  • Action은 ViewModel이 가지고 있다가 Coordinator를 호출할 때 사용
  • ViewModel에서는 Coordinator에 의존하지 않게끔하여 화면전환 코드에 의존하지 않게끔 설계

  • Coordinator와 ViewModel에서의 Action관련 코드

예제) Red, Blue 버튼을 누르면 해당 ViewController로 이동하는 앱

  • Coordinator와 Action을 사용하여 구현

  • 구조 설계

  • ButtonViewModel에 존재하는 ButtonsViewModelActions
struct ButtonsViewModelActions {
    let showRedButton: (Int) -> Void
    let showBlueButton: (Int) -> Void
}

...

final class ButtonsViewModelImpl: ButtonsViewModel {
    private let actions: ButtonsViewModelActions
    ...
    
    init(actions: ButtonsViewModelActions, buttonUseCase: ButtonsUseCase) {
        self.actions = actions
        self.buttonsUseCase = buttonUseCase
    }
    
    ...
    
    // Input

    func didTapRedButton() {
        buttonsUseCase.redButtonCnt += 1
        actions.showRedButton(buttonsUseCase.redButtonCnt)
    }

    func didTapBlueButton() {
        buttonsUseCase.blueButtonCnt += 1
        actions.showBlueButton(buttonsUseCase.blueButtonCnt)
    }
}
  • Action은 Coordinator에서 구현
protocol ButtonsCoordinatorDependencies {
    func makeButtonViewController(actions: ButtonsViewModelActions) -> ButtonsViewController

    func makeBlueViewController(tapCount: Int) -> UIViewController
    func makeRedViewController(tapCount: Int) -> UIViewController
}

final class ButtonsCoordinator {
    private weak var navigationController: UINavigationController?
    private let dependencies: ButtonsCoordinatorDependencies

    init(navigationController: UINavigationController,
         dependencies: ButtonsCoordinatorDependencies) {
        self.navigationController = navigationController
        self.dependencies = dependencies
    }

    func start() {
        let actions = ButtonsViewModelActions(showRedButton: showRed(tapCount:),
                                              showBlueButton: showBlue(tapCount:))
        let vc = dependencies.makeButtonViewController(actions: actions)
        navigationController?.pushViewController(vc, animated: false)
    }

    // Private

    private func showRed(tapCount: Int) {
        let vc = dependencies.makeRedViewController(tapCount: tapCount)
        navigationController?.pushViewController(vc, animated: true)
    }

    private func showBlue(tapCount: Int) {
        let vc = dependencies.makeBlueViewController(tapCount: tapCount)
        navigationController?.pushViewController(vc, animated: true)
    }
}
  • action을 통해 화면 전환이 되는데, 화면전활 할때 필요한 ViewController는 DIContainer에서 구현
    • extension으로 구현 후, makeButtonCoordinator에서 dependencies: self로 주입
class ButtonsDIContainer {

    func makeButtonCoordinator(navigationController: UINavigationController) -> ButtonsCoordinator {
        return ButtonsCoordinator(navigationController: navigationController, dependencies: self)
    }

    // Private

    private func makeButtonsUseCaseImpl() -> ButtonsUseCase {
        return ButtonsUseCaseImpl()
    }

    private func makeButtonsViewModel(actions: ButtonsViewModelActions) -> ButtonsViewModel {
        return ButtonsViewModelImpl(actions: actions, buttonUseCase: makeButtonsUseCaseImpl())
    }
}

extension ButtonsDIContainer: ButtonsCoordinatorDependencies {
    func makeButtonViewController(actions: ButtonsViewModelActions) -> ButtonsViewController {
        return ButtonsViewController.create(with: makeButtonsViewModel(actions: actions))
    }

    func makeRedViewController(tapCount: Int) -> UIViewController {
        return RedViewController.create(tapCount: tapCount)
    }

    func makeBlueViewController(tapCount: Int) -> UIViewController {
        return BlueViewController.create(tapCount: tapCount)
    }
}
  • 첫 화면은 DI와 Coordinator를 이용하여 이동
let buttonsDIContainer = ButtonsDIContainer()
let coordinator = buttonsDIContainer.makeButtonCoordinator(navigationController: navigationController!)

coordinator.start()

* 전체 소스 코드 `Coordinator Pattern` 하위 그룹에 존재: https://github.com/JK0369/SOLID

Comments