Coordinator?
코디네이터 패턴을 제시한 Soroush Khanlou씨는 코디네이터를 다음과 같이 설명합니다.
하나 이상의 뷰 컨트롤러에게 뷰의 트랜지션을 지시하는 객체다!
약간 말이 어렵지만,
하나 이상의 뷰 컨트롤러의 화면 전환, 계층에 대한 흐름을 제어하는 객체라고 생각하시면 됩니다.
일반적으로 뷰 컨트롤러를 전환한다고 하면 이러한 모습을 가지고 있죠?
뷰 컨트롤러A는 뷰 컨트롤러 B에 대해 알고 있어야 하는 구조입니다!
하지만 Coordinator 패턴에서
뷰 컨트롤러A는 Coordinator 인스턴스만 가지고 있고, 뷰 컨트롤러 B에 대해서는 알 필요가 없습니다!
화면 전환을 하고 싶으면 Coordinator에게 요청을 하는 구조입니다.
어떠한 장점이 있을까?
일반적으로 코드를 작성할 때,
뷰 컨트롤러에 다음 화면을 보여주는 로직을 작성하겠죠? (push든 present든,,)
화면 전환에 대한 책임이 뷰 컨트롤러에 있습니다!
뷰 컨트롤러는 고생이 참 많습니다. 그래서 이 고생을 Coordinator가 덜어주기로 한 거죠!
이제 화면 전환과 의존성 주입은 이 Coordinator가 담당하기로 했습니다.
맛을 보자!
먼저 Coordinator의 구조를 보고 갑시다!
// Coordinator.swift
protocol Coordinator {
// 자식 Coordinator를 담는 배열
var childCoordinators: [Coordinator] { get set }
// init 대신 var navigationController: UINavigationController { get set } 으로 구현하셔도 됩니다!
init(navigationController: UINavigationController)
func start()
}
자식 코디네이터를 담는 배열이 있고, init으로 UINavigationController를 받고 있네요.
start() 메소드는 해당 뷰 컨트롤러를 보여주는 역할을 담당합니다.
이 프로토콜을 사용해서 MainCoordinator를 작성하면...
// MainCoordinator.swift
class MainCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
unowned let navigationController: UINavigationController
required init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let MainViewController: MainViewController = MainViewController()
MainViewController.delegate = self
self.navigationController.viewControllers = [MainViewController]
}
}
이런 식으로 구현을 할 수 있습니다!
보시면 MainViewController.delegate = self가 보이죠?
코디네이터 패턴에서 델리게이트 패턴을 사용하고 있는 걸 알 수 있습니다!
그런데 저 delegate는 뭐죠?
// MainViewController.siwft
protocol MainViewControllerDelegate: class {
func navigateToNextPage()
}
class MainViewController: UIViewController {
var disposeBag = DisposeBag()
public weak var delegate: MainViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
bind()
}
// MARK: Binding
private func bind() {
button.rx.tap
.subscribe(onNext: { [weak self] in
guard let `self` = self else {return}
self.delegate?.navigateToNextPage()
}).disposed(by: disposeBag)
}
...
}
네! 그냥 다음 뷰 컨트롤러를 push 혹은 present 할 때 필요한 프로토콜입니다!
델리게이트 패턴이기 때문에 역시 순환 참조를 잘 피해줘야겠죠?
그래서 weak var로 delegate를 선언해 줍니다.
저기 button.rx.tap 어쩌구는 버튼 클릭 이벤트를 구현한 로직입니다.
버튼을 클릭하면 navigateToNextPage() 메소드를 수행합니다!
뭐야! 화면 전환 로직은 어디갔어!
다음 뷰 컨트롤러가 뭐야!
네, 위에서 말씀드린 것처럼 화면 전환 로직은 Coordinator로 옮겨졌습니다!
여기서 또 아셔야 할 것은 뷰 컨트롤러는 다음 혹은 이전 뷰 컨트롤러를 모른다는 것입니다. (독립적이다악!)
다음 뷰 컨트롤러로 넘어가는 로직은 MainCoordinator에 작성하도록 합시다.
// MainCoordinator.swift
...
extension MainCoordinator: MainViewControllerDelegate {
func navigateToNextPage() {
let nextCoordinator = SecondCoordinator(navigationController: navigationController)
// 이 delegate는 다시 main으로 돌아올 때 사용합니다. 아래에서 구현하겠습니다!
nextCoordinator.delegate = self
childCoordinators.append(nextCoordinator)
nextCoordinator.start()
}
}
보기 편하게 extension으로 navigateToNextPage() 메소드를 따로 구현하였습니다.
(SecondCoordinator는 아래에서 구현하겠습니다!)
MainViewControllerDelegate 프로토콜을 채택한 후, navigateToNextPage() 메소드의 로직을 구현합니다.
아까 ChildCoordinator 배열에 자식 코디네이터들을 넣어준다고 했었죠?
그 로직도 함께 넣어줍니다.
start로 해당 코디네이터에서 보여줄 뷰 컨트롤러를 다룬다고 했으니
마지막에는 다음 코디네이터의 start() 메소드를 실행해 줍니다!
이 MainViewController가 앱의 첫 화면이라면 MainCoordinator는 어디에서 호출해야 할까요?
네! AppDelegate입니다.
// AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var mainCoordinator: MainCoordinator?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
guard let window = window else { return false }
window.rootViewController = UINavigationController()
mainCoordinator = MainCoordinator(navigationController: window.rootViewController! as! UINavigationController)
mainCoordinator?.start()
window.backgroundColor = .white
window.makeKeyAndVisible()
return true
}
}
사용하는 방법은 위에서 nextCoordinator를 호출했던 방법과 같습니다!
루트 뷰 컨트롤러를 생성해서 코디네이터에 넣어주고,
해당 코디네이터의 start() 메소드를 호출하면 됩니다.
나머지 window와 관련된 코드들은 스토리보드를 사용하지 않기 때문에 작성한 것입니다.
자, 순서를 다시 정리를 해보면,
- 앱 실행
- MainViewController 인스턴스 생성
- 코디네이터에 해당 인스턴스(MainViewContoller)를 넣어줌
- start() 메소드로 MainViewController를 push
- 짜잔~ MainViewController의 뷰가 화면에 보여진다!
- MainViewController의 버튼을 클릭한다!
- MainViewControllerDelegate 프로토콜의 navigateToNextPage() 메소드를 수행한다.
- 그런데 이 메소드 로직은 MainCoordinator에 있다! (화면 전환 책임은 코디네이터에!)
- 그 이후 로직은 2번 ~ 5번과 같습니다 (인스턴스 생성하고 어쩌구... start()로 push하고 어쩌구...)
다음 페이지로 넘어가는 로직(push / present)이 있다면
다시 돌아오는 로직(pop / dismiss)도 필요하겠죠?
우선 SecondViewController를 구현합시다!
MainViewController와 같고, 거기에 돌아가는 코드만 추가하면 됩니다.
// SecondViewContoller.swift
protocol SecondViewContollerDelegate: AnyObject {
// 다시 메인으로 돌아가버렷
func navigateToMainPage()
// 다음 뷰로 넘어갈 경우 사용
func navigateToNextPage()
}
class SecondViewContoller: UIViewController {
var disposeBag = DisposeBag()
public weak var delegate: SecondViewContollerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
bind()
}
// MARK: Binding
private func bind() {
button.rx.tap
.subscribe(onNext: { [weak self] in
self?.delegate?.navigateToMainPage()
}).disposed(by: disposeBag)
}
...
}
그 후, 화면 전환 로직을 작성하기 위해 SecondCoordinator를 작성합시다!
protocol BackToMainViewControllerDelegate: AnyObject {
func navigateToMainPage(newOrderCoordinator: SecondCoordinator)
}
class SecondCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
weak var delegate: BackToMainViewControllerDelegate?
unowned let navigationController: UINavigationController
// 여기에 들어오는 navigationController는 AppDelegate에서 생성한 navigationController 입니다!!
required init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let secondViewContoller = SecondViewContoller()
secondViewContoller.delegate = self
navigationController.pushViewController(secondViewContoller, animated: true)
}
}
extension SecondCoordinator: SecondViewContollerDelegate {
func navigateToMainPage() {
self.delegate?.navigateToMainPage(newOrderCoordinator: self)
}
func navigateToNextPage() {
// 다음 페이지를 위한 로직 (인스턴스 생성 ~ start())
}
}
마지막으로 다시 MainCoordinator로 돌아와서 BackToMainViewControllerDelegate() 로직을 작성합시다.
// MainCoordinator.swift
...
extension MainCoordinator: BackToMainViewControllerDelegate {
func navigateToMainPage(newOrderCoordinator: SecondCoordinator) {
navigationController.popToRootViewController(animated: true)
childCoordinators.removeLast()
}
}
push를 하면 자식 코디네이터를 배열에 추가하고, 반대로 pop을 한다면 배열에서 제거하는 방식입니다.
뷰는 스택으로 쌓이기 때문에 pop을 한다면 배열의 맨 마지막 코디네이터를 빼줘야겠죠?
그래서 .removeLast()를 사용했습니다!
이 예제에서는 한 뷰 컨트롤러에서 한 개의 화면 전환만 이루어졌기 때문에 navigateToNextPage라는 메소드 명을 지었지만,
정확한 뷰의 이름으로 네이밍을 하시는 것이 좋습니다! (화면 전환이 많아지면 찾기 어렵다!)
아직 다루지 않은 의존성 주입은 다음에 알아보도록 하겠습니다!
코드가 누락된 부분이 있을 수도 있기 때문에 깃허브 링크를 남기겠습니다.
https://github.com/LeeMyungJic/Coordinator_Exsample
'iOS > 디자인패턴' 카테고리의 다른 글
디자인패턴 - 싱글턴 패턴(Singleton Pattern) (0) | 2023.02.01 |
---|---|
swift - MVVM 패턴 (feat. MVC) (1) | 2022.05.20 |