Coordinator 문제점과 느낀점..
Coordinator
0. Coordinator의 수행 기능
1) 화면 전환에 필요한 인스턴스 생성 (UIViewController, ViewModel 등)
2) 생성한 인스턴스의 종속성 주입 (DI)
3) 생성된 UIViewController의 화면 전환 (push or present)
1. Coordinator Protocol 구현
import UIKit
protocol Coordinator: AnyObject {
var navigationController: UINavigationController { get set }
var childCoordinators: [Coordinator] { get set }
func start()
}
navigationController
: 화면 전환에 필요한 UINavigationController
navigationController.pushViewController(vc, animated: true) // push
navigationController.present(vc, animated: true, completion: nil) //present
chlidCoordinators
: 화면 전환시 생성될 하위 Coordinator를 저장할 때 사용합니다.
coordinator 생성 후 저장하지 않으면, 메모리에서 제거되기 때문에 꼭 저장해야합니다.
start
: 컨트롤러 생성, 화면 전환 및 종속성 주입의 역할을 합니다.
Coordinator + Extension
extension Coordinator {
public func addChildCoordinator(_ childCoordinator: Coordinator) { // 코디네이터 추가
self.childCoordinators.append(childCoordinator)
}
public func removeChildCoordinator(_ childCoordinator: Coordinator) { // 코디네이터 삭제, 네비게이션 스택에 코디네이터가 필요하지 않은 경우에만 삭제합니다
self.childCoordinators = self.childCoordinators.filter { $0 !== childCoordinator }
}
public func removeChildCoordinators() { // 코디네이터 전체 삭제
childCoordinators.forEach { $0.removeChildCoordinators() }
childCoordinators.removeAll()
}
}
BaseCoordinator
중복되는 프로퍼티들이 많기 때문에 BaseCoordinator를 만들어 상속받아 사용합니다.
import UIKit
class BaseCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
fatalError("Start method must be implemented")
}
}
2. AppCoordinator
import UIKit
public enum AppFlow {
case login
case main
}
final class AppCoordinator: Coordinator {
var navigationController: UINavigationController
var childCoordinators: [Coordinator]
let window: UIWindow
let flow: AppFlow
init(window: UIWindow) {
navigationController = UINavigationController()
self.window = window
self.window.backgroundColor = .white
self.window.rootViewController = navigationController
childCoordinators = []
flow = .login
}
func start() {
switch flow {
case .login:
let loginCoordinator = LoginCoordinator(navigationController: self.navigationController, dependencies: self)
loginCoordinator.start()
addChildCoordinator(loginCoordinator)
case .main:
let mainCoordinator = MainCoordinator(navigationController: self.navigationController)
mainCoordinator.start()
addChildCoordinator(mainCoordinator)
}
window.makeKeyAndVisible()
}
}
extension AppCoordinator: LoginCoordinatorDependencies {
func makeMainTabBarViewController(_ loginCoordinator: LoginCoordinator) {
window.rootViewController = navigationController
removeChildCoordinator(loginCoordinator)
let mainCoordinator = MainCoordinator(navigationController: self.navigationController)
mainCoordinator.start()
addChildCoordinator(mainCoordinator)
window.makeKeyAndVisible()
}
}
AppCoordinator는 앱 시작시 초기 컨트롤러를 연결하기 위한 coordinator입니다.
AppDelegate에서 전달받은 window의 rootViewController와 Main또는 Login을 연결하기 위한 내용입니다.
- window.rootViewController = navigationController 초기화 메서드에서 생성한 UINavigationController를 rootViewController로 설정
- let loginCoordinator = LoginCoordinator(navigationController: self.navigationController, dependencies: self) rootViewController로 설정된 navigationController를
LoginCoordinator
에 생성자 파라미터로 전달하여,LoginViewController
와 연결하고,
LoginCoordinator
에 자신을 전달(LoginCoordinator
의 부모 코디네이터가 AppCoordinator임을 알리기 위함 → Delegate 대신, appCoordinator(부모 코디네이터, parentCoordinator)를 자식 코디네이터 생성시에 전달하는 경우도 있습니다.
- childCoordinators.append(coordinator) coordinator가 메모리에서 사라지지 않기 위해서 인스턴스를 저장해야한다.
- coordinator.start() start()를 호출하여 LoginVC 생성
3. LoginCoordinator
import UIKi
enum LoginFlow {
case main
case yellow
}
protocol LoginCoordinatorDependencies: AnyObject {
func makeMainTabBarViewController(_ loginCoordinator: LoginCoordinator)
}
final class LoginCoordinator: BaseCoordinator {
weak var dependencies: LoginCoordinatorDependencies?
init(navigationController: UINavigationController, dependencies: LoginCoordinatorDependencies) {
super.init(navigationController: navigationController)
self.dependencies = dependencies
}
override func start() {
let viewModel = LoginViewModel(loginControllable: self) // 코디네이터에서 ViewModel을 생성후
let login = LoginViewController(loginViewModel: viewModel) // LoginVC에 VM을 주입해줍니다.
login.title = "로그인"
self.navigationController.pushViewController(login, animated: true)
}
}
extension LoginCoordinator: LoginViewControllable {
func performTransition(_ loginViewModel: LoginViewModel, to transition: LoginFlow) {
switch transition {
case .main:
dependencies?.makeMainTabBarViewController(self)
case .yellow:
let yellow = YellowCoordinator(navigationController: navigationController)
yellow.start()
yellow.dependencies = self
addChildCoordinator(yellow)
}
}
}
extension LoginCoordinator: YellowCoordinatorDependencies {
func performTransition(_ yellowCoordinator: YellowCoordinator, to transition: YellowFlow) {
switch transition {
case .main:
dependencies?.makeMainTabBarViewController(self)
case .pop:
removeChildCoordinator(yellowCoordinator)
navigationController.popViewController(animated: true)
case .red:
let red = RedCoordinator(navigationController: navigationController)
red.start()
addChildCoordinator(red)
}
}
}
4. LoginViewController
import UIKit
import RxCocoa
import RxSwift
final class LoginViewController: UIViewController {
private let viewModel: LoginViewModel
private let button: UIButton = {
let btn = UIButton()
btn.backgroundColor = .black
btn.setTitle("메인으로 이동", for: .normal)
btn.translatesAutoresizingMaskIntoConstraints = false
return btn
}()
private let button2: UIButton = {
let btn = UIButton()
btn.backgroundColor = .black
btn.setTitle("노랑으로 이동", for: .normal)
btn.translatesAutoresizingMaskIntoConstraints = false
return btn
}()
init(loginViewModel: LoginViewModel) {
self.viewModel = loginViewModel
super.init(nibName: nil, bundle: nil)
bind()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setUpButton()
}
private func bind() {
let input = LoginViewModel.Input(
buttonDidTapped: button.rx.controlEvent(.touchUpInside).asSignal(), // tap 이벤트를 ViewModel로 전달합니다
button2DidTapped: button2.rx.controlEvent(.touchUpInside).asSignal()
)
_ = viewModel.transform(input: input)
}
private func setUpButton() {
view.addSubview(button)
button.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor, constant: 0).isActive = true
button.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor, constant: 0).isActive = true
button.widthAnchor.constraint(equalToConstant: 117).isActive = true
button.heightAnchor.constraint(equalToConstant: 40).isActive = true
view.addSubview(button2)
button2.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor, constant: -30).isActive = true
button2.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor, constant: 0).isActive = true
button2.widthAnchor.constraint(equalToConstant: 117).isActive = true
button2.heightAnchor.constraint(equalToConstant: 40).isActive = true
}
}
5. LoginViewModel
import RxSwift
import RxCocoa
protocol LoginViewControllable: AnyObject {
func performTransition(_ loginViewModel: LoginViewModel, to transition: LoginFlow)
}
final class LoginViewModel {
private let disposeBag = DisposeBag()
weak var controllable: LoginViewControllable?
struct Input {
let buttonDidTapped: Signal<Void>
let button2DidTapped: Signal<Void>
}
struct Output {
}
init(loginControllable: LoginViewControllable) {
self.controllable = loginControllable
}
func transform(input: Input) -> Output {
input.buttonDidTapped
.withUnretained(self)
.emit{ owner, _ in
owner.controllable?.performTransition(owner, to: .main)
}
.disposed(by: disposeBag)
input.button2DidTapped
.withUnretained(self)
.emit{ owner, _ in
owner.controllable?.performTransition(self, to: .yellow)
}
.disposed(by: disposeBag)
return Output()
}
}
6. 궁금증
코디네이터와 VC 는 무조건 1:1 인가?
: 자료들을 찾다보면 1:N인 경우도, 1:1인 경우도 있습니다.
코디네이터와 VC를 1:1로 하는 이유?
: 코디네이터는 VC를 생성해주고 ViewModel을 만들어 주입하기 때문입니다.
코디네이터와 VC는 무조건 1:1로 하되 부모 코디네이터와 자식 코디네이터를 명확하게 구분해주세요!
코디네이터의 장점
- 화면 전환만 담당하는 역할로 분리가 가능하다.
- ViewModel에서 다음 VC를 띄울 수 있다. → ViewModel1 - VC1 - VC2 - ViewModel2 의 데이터 흐름이아닌 → ViewModel1 - 코디네이터 - ViewModel2로 데이터 전달이 가능
코디네이터의 단점
- 개념자체는 간단하지만 복잡하다. (화면 이동 3줄이면 하는데 코디네이터를 사용하게 되면 생성되는 파일만 몇갠지..)
- Delegate 패턴을 많이 사용하므로 한번 화면이 꼬이기 시작하면 끝도없다...
- 레퍼런스마다 방식이 달라서 어렵다.
7. 문제점🌟🌟🌟
구글링하며 찾은 레퍼런스들은 모두 버튼에 대한 동작에 대해서만 코디네이터를 작동시키고 있었다.
하지만 “숨쉴때” (숭실대학교 커뮤니티 앱)에서는 기본 네비바를 사용하고 있었기 때문에 제스쳐에 대한 dimsiss나 pop과 네비게이션 컨트롤러에서 만들어주는 백버튼을 눌렀을때 pop되는 부분은 처리하지 못했다.
viewDidDisappear를 살펴보자!
viewController는 자체적으로 dismiss 되는지 또는 pop되는지에 대해 알 수 있다.
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if navigationController?.isBeingDismissed ?? false {
print("Red: navigationController isBeingDismissed")
}
if navigationController?.isMovingFromParent ?? false {
print("Red: navigationController isMovingFromParent")
}
if isBeingDismissed {
print("isBeingDismissed")
}
if isMovingFromParent {
print("isMovingFromParent")
}
}
isBeingDismissed
isMovingFromParent
두 가지를 사용하면 pop과 dismiss 되는 경우를 알아낼 수 있다.
네비게이션 자체를 present , push 해주는 경우도 있으므로 경우의 수는 4가지이다.
이를 토대로 Rx+UIViewController extension을 작성해보자
var isDismissing: Observable<Void> {
return base.rx
.methodInvoked(#selector(Base.viewDidDisappear(_:)))
.map { _ in }
.filter { [weak base] in
guard let base = base else { return false}
return base.isBeingDismissed
}
}
var isPopping: Observable<Void> {
return base.rx
.methodInvoked(#selector(Base.viewDidDisappear(_:)))
.map { _ in }
.filter { [weak base] in
guard let base = base else { return false}
return base.isMovingFromParent
}
}
var isDismissingWithNavigationController: Observable<Void> {
return base.rx
.methodInvoked(#selector(Base.viewDidDisappear(_:)))
.map { _ in }
.filter { [weak base] in
guard let base = base else { return false}
guard let navigationController = base.navigationController else { return false }
return navigationController.isBeingDismissed
}
}
var isPoppingWithNavigationController: Observable<Void> {
return base.rx
.methodInvoked(#selector(Base.viewDidDisappear(_:)))
.map { _ in }
.filter { [weak base] in
guard let base = base else { return false}
guard let navigationController = base.navigationController else { return false }
return navigationController.isMovingFromParent
}
}
ViewController에서
rx.isPopping
.map { _ in (print("finish")) }
.bind(to: viewModel.finish)
.disposed(by: disposeBag)
이런식으로 바인딩 해준뒤에
ViewModel에서
finish
.withUnretained(self)
.subscribe { owner, _ in
owner.controllable?.finish()
}
.disposed(by: disposeBag)
이런식으로 viewController가 종료되었다는 것을 알려주면 된다.
8. 느낀점
사실 화면전환을 하기 위해 이렇게나 많은 클래스와 파일을 생성해야한다는데 가장 큰 단점인 것 같다. 또한 생명주기에 바인딩하는 것도 비효율적이라 생각하기도하고... 경우에 따라서는 코디네이터를 사용하지 않는 것이 더 좋을 수도 있다고 생각한다.. 더 좋은 구현방법이 있으면... 알려주세용..
그래도 ViewController가 아닌 ViewModel에서 Coordinator에게 알려 화면전환을 한다는 점은 큰 장점으로 작용하는 것 같다..! 코디네이터를 통해서 역할분리가 완전 되는 느낌이랄까..
Author And Source
이 문제에 관하여(Coordinator 문제점과 느낀점..), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@ezidayzi/Coordinator-문제점과-느낀점저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)