jane1choi / TIL

Today I Learned #심야아요

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Architecture] Coordinator Pattern

jane1choi opened this issue · comments

Coordinator 패턴의 등장 이유

iOS에서는 화면 전환을 담당하는 컨트롤러인 UINavigationController가 있습니다.
Stack 방식으로 새로운 화면을 push하고, 이전화면으로 돌아가가기 위해 pop해서 화면전환을 해줍니다.
가장 첫 화면을 기준으로 새로운 화면으로 넘어갈때 마다 순서대로 쌓이고, 뒤로가기 버튼을 통해 이전에 방문했던 화면들을 순서대로 꺼낼 수 있는 방식입니다.

navigationController?.pushviewController(nextViewController, animated:true)

push, pop을 이용하여 쉽고 간단하게 화면을 전환 할 수 있지만, 단점이 존재합니다.
앱이 점차 커지고 화면이 많아져 화면전환 플로우가 복잡해지게 되면 네비게이션 스택관리가 어려워지고,
화면을 전환하는 코드가 사용하는 view controller를 의존하기 때문에 VC가 무거워지게 됩니다..
이러한 문제를 해결하기 위해서 Coordinator 패턴을 도입하게 된 것입니다.

Coordinator Pattern이란?

Coordinator 패턴은 ViewController로 부터 화면 전환의 부담을 줄여주고,
화면전환을 보다 더 관리하기 쉽도록 도와주기 위한 패턴입니다. (화면의 흐름을 제어해주는 역할, 라우팅, VC관리)
images-ellyheetov-post-6ec003e2-ef4b-4a4f-a326-199d96484867-Screen Shot 2021-06-15 at 5 35 05 PM
Coordinator 패턴을 사용하면, ViewController 사이에 결합도를 낮춰 줄 수 있다는 장점이 있습니다.
화면전환은 Coordinator가 모두 관리하기 때문에, 각 ViewController는 이전에 어떤 컨트롤러가 있었는지, 다음에 어떤 컨트롤러가 오는지 알 필요가 없습니다. Coordinator만이 이것을 알고 관리합니다.
결과적으로, 어떠한 순서로든 컨트롤러 전환이 가능하고, 재사용 까지도 가능하여 hard-coding을 피할 수 있게 됩니다.

정리하자면, Coordinator를 사용하면 화면 전환에 대한 로직을 따로 분리할 수 있기 때문에 VC의 책임을 줄여줄 수 있다는 것이 가장 큰 장점입니다. 또한 의존성 주입을 해줄 수 있는 허브 역할을 할 수 있다는 장점도 있습니다.

Coordinator 구현

먼저 코디네이터를 이용한 화면전환의 흐름을 간단히 요약하고, 간단한 코드로 구현해보겠습니다!
각각의 View 담당 Coordinator가 다음 화면을 띄울 때 Main Coordinator에게 요청을 보내고 이 요청을 받은 Main Coordinator는 응답으로 화면을 전환시켜줍니다.

코드로 구현해보자!

MainVC에서 SecondVC로 코디네이터를 이용해서 화면전환을 구현해보도록 하겠습니다!

Protocol 생성

protocol Coordinator { 
	var childCoordinators: [Coordinator] { get set }
	var navigationController: UINavigationController { get set } 

	func start() 
}

먼저, 프로토콜을 생성해줍니다.

이 프로토콜은 자식 Coordinator를 가지고 있을 배열과 navigation 스택을 쌓을 UINavigationController 타입의 변수,
그리고 시작 시 실행될 함수(화면 전환을 수행)를 가지고 있습니다.

MainCoordinator

class MainCoordinator: Coordinator { 
	var childCoordinators = [Coordinator]() 
	var navigationController: UINavigationController 

	init(nav: UINavigationController) { 
		self.navigationController = navigationController 
	} 

	func start() {
		let mainViewController = MainViewController()
		mainViewController.coordinator = self 
		navigationController.pushViewController(mainViewController, animated: true) // 화면전환
	} 
		
	func pushSecondVC() {
		let secondViewCoordinator = SecondViewCoordinator(navigationController: navigationController)
		secondViewCoordinator.parentCoordinator = self
		childCoordinator.append(secondViewCoordinator) 
		secondViewCoordinator.start() // start() 함수 호출
	}
}

프로토콜을 채택해서 클래스를 만들어주고 start()함수를 정의해주었습니다.
init 생성자의 인자로 navigationController를 넣어주어 초기화를 해주고, start() 메서드를 통해 MainViewController(첫 화면)를 띄우게 됩니다. start()를 호출하면 해당하는 VC가 Navigation에 쌓이게 되겠네요!
pushSecondVC() 함수는 두번째 화면이 보여지도록 하는 함수입니다.
따라서 MainCoordinator에서는 start, pushSecondVC 이 두 함수 통해서 2개의 VC를 관리하고 있습니다.

SecondViewCoordinator

class SecondViewCoordinator: Coordinator {
	var childCoordinators: [Coordinator] = []
	var navigationController: UINavigationController
	weak var parentCoordinator: MainCoordinator?

	init(navigationController: UINavigationController) {
		self.navigationController = navigationController
	}

	func start() { // 프로토콜의 함수 구현
		let secondVC = SecondViewController()
		secondVC.coordinator = self
		navigationController.pushViewController(secondVC, animated: true)
	}
}

SecondViewCoordinator(메인 코디네이터에 대한 자식 코디네이터)에서는 SecondViewController에 대한 화면 전환 로직을 제어할 코디네이터를 만들어줍니다.
메인 코디네이터에서와 똑같이 호출되면 화면을 전환할 수 있도록 start()함수를 구현해줍니다.

AppDelegate 설정

var window: UIWindow?
var mainCoordinator: MainCoordinator?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let navigationController = UINavigationController()
	mainCoordinator = MainCoordinator(navigationController: navigationController)
	mainCoordinator?.start()
	
	window = UIWindow(frame: UIScreen.main.bounds)
	window?.rootViewController = navigationController
	window?.makeKeyAndVisible()

        return true
}

Coordinator를 이용해 화면 전환을 구현하기 위해서는 Appdelegate의 didFinishLaunchingWithOptions 메서드에서 설정이 필요하다고 합니다!
이 메서드에서 MainCoordinator 생성을 위해 필요한 NavigationController 객체를 생성하고 이를 기반으로 MainCoordinator 객체를 생성합니다. 이후, start() 메서드를 호출하여 해당 NavigationController에 MainViewController를 push합니다.
이를 통해 앱에서 런칭이 끝나면 MainViewController가 화면에 보여지게 됩니다!

화면 전환을 위한 준비를 다 해주었습니다.
이제 VC에서 실제 화면전환을 해보도록 할게요!

MainViewController

MainViewController의 View 안의 버튼을 터치하면 SecondViewController를 띄워보도록 할게요!

class MainViewController: UIViewController {
	weak var coordinator: MainCoordinator?
	weak var pushButton: UIButton!

	let disposeBag = DisposeBag()
		
	override func viewDidLoad() {
		super.viewDidLoad()
		setupViews()
		setupLayoutConstraints()
		pushButton.rx.tap.asDriver(onErrorJustReturn: ())
				.drive(onNext: { [weak self] in
						self?.coordinator?.pushSecondVC()
				})
				.disposed(disposeBag)
	}

	func setupView() {
		view.backgroundColor = .systemBlue
		let pushButton = UIButton()
		pushButton.setTitle("push", for: .normal)
		self.pushButton = pushButton)
	}

	func setupLayoutConstraints() {
		pushButton.snp.makeConstraints { make in
			make.center.equalToSuperview()
			make.size.equalTo(CGSize(width: 100, height: 50))
		}
	}
}

MainViewController의 뷰에 버튼을 하나 만들고 RxCocoa를 이용해 해당 버튼의 터치 이벤트를 구독했습니다.
버튼의 터치 이벤트가 방출되게 되면 coordinator의 메서드를 통해 메서드를 호출하여 SecondVC로 화면을 전환합니다!

MainVC로의 이동은 MainCoordinator가, SecondVC로의 이동은 SecondViewCoordinator가 담당하고 있습니다.

마지막으로 흐름을 정리해보자면,
MainVC의 버튼 터치 시MainCoordinator.pushSecondVC() → SecondViewCoordinator.start()의 흐름으로 동작합니다.

참고자료: https://nsios.tistory.com/48, https://duwjdtn11.tistory.com/644