1. protocol Coordinator
AppCoordinator
를 제외하고 모두 부모 코디네이터를 갖는다navigationController
를 갖는다.enum CoordinatorType
의 case에 존재하고, 각 코디네이터마다 본인의 타입을 저장하는 프로퍼티가 있다.extension Coordinator
에서 메서드를 선언한다.protocol Coordinator: AnyObject {
// 1. 부모 코디네이터
var finishDelegate: CoordinatorFinishDelegate? { get set }
// 2. 각 코디네이터는 하나의 nav를 갖는다
var navigationController: UINavigationController { get set }
init(_ navigationController: UINavigationController)
// 3. 현재 살아있는 자식 코디네이터 배열.
var childCoordinators: [Coordinator] { get set }
// 4. Flow 타입
var type: CoordinatorType { get }
// 5. Flow 시작 시점 로직
func start()
// 6. Flow 종료 시점 로직. (extension에서 선언)
func finish(_ nextFlow: ChildCoordinatorTypeProtocol?)
}
extension Coordinator {
func finish(_ nextFlow: ChildCoordinatorTypeProtocol?) {
// 1. 자식 코디 다 지우기
childCoordinators.removeAll()
// 2. 부모 코디에게 알리기
finishDelegate?.coordinatorDidFinish(
childCoordinator: self,
nextFlow: nextFlow
)
}
}
2. protocol CoordinatorFinishDelegate: AnyObject
protocol CoordinatorFinishDelegate: AnyObject {
func coordinatorDidFinish(childCoordinator: Coordinator, nextFlow: ChildCoordinatorTypeProtocol?)
}
// ex.
extension AppCoordinator: CoordinatorFinishDelegate {
func coordinatorDidFinish(
childCoordinator: Coordinator,
nextFlow: ChildCoordinatorTypeProtocol?
) {
// 1. 자식 코디 배열 초기화 (사실상 하나밖에 없었기 때문에 빈 배열이 된다)
childCoordinators = childCoordinators.filter { $0.type != childCoordinator.type }
// 2. nav에 연결된 VC 모두 제거
navigationController.viewControllers.removeAll()
// 3. 다음 타겟 코디 실행
// 3 - 0
if nextFlow == nil { }
// 3 - 1. Child 코디인 경우
else if let nextFlow = nextFlow as? ChildCoordinatorType {
/* ... */
}
// 3 - 2. Child 코디보다 더 하위 코디인 경우 -> 직접 특정해야 한다
// ex). 탭바의 자식 코디인 경우
else if let nextFlow = nextFlow as? TabBarCoordinator.ChildCoordinatorType {
/* ... */
}
// 3 - 3. 부모 코디를 타고 가야 하는 경우
else {
self.finish(nextFlow)
}
}
3. protocol ChildCoordinatorTypeProtocol
enum
으로 저장한다.enum
으로 다음 코디네이터를 특정한다.coordinatorDidFinish
메서드에서는 모든 자식 코디네이터를 매개변수로 받을 수 있어야 하고, 이를 위해 이 프로토콜을 선언한다.enum
은 이 프로토콜을 채택한다protocol ChildCoordinatorTypeProtocol {
}
// ex.
// AppCoordinator
extension AppCoordinator {
enum ChildCoordinatorType: ChildCoordinatorTypeProtocol {
case splash
case loginScene
case tabBarScene
case homeEmptyScene
}
}
4. CoordinatorType
enum CoordinatorType {
case app
case splash, loginScene, homeEmptyScene, tabBarScene
/* ... */
}
// LoginSceneCoordinator (Onboarding View)
class LoginSceneCoordinator: LoginSceneCoordinatorProtocol {
/* ... */
func showSelectAuthFlow() {
// TODO: selectAuthFlow 시작. present로 진행. 별도의 navigationController 주입
// 새로 만드는 nav는 어차피 여기서 관리하지는 않을거고, 새로운 코디에서 관리할 예정이기 때문에 현재 코디에서 따로 변수로 가지고 있을 필요는 없다
// 단지, 현재 코디와 다른 nav를 가질 수 있도록 주입해주는 것 뿐임
let nav = UINavigationController()
let selectAuthCoordinator = SelectAuthCoordinator(nav)
selectAuthCoordinator.finishDelegate = self
childCoordinators.append(selectAuthCoordinator)
selectAuthCoordinator.start() // 코디의 첫 화면 세팅
// 여기서 직접 present 실행
navigationController.present(selectAuthCoordinator.navigationController, animated: true)
}
}
// SelectAuthCoordinator (SelectAuth View)
class SelectAuthCoordinator: SelectAuthCoordinatorProtocol {
/* ... */
func start() {
showSelectAuthView()
}
// 초기 화면
func showSelectAuthView() {
let selectAuthVM = /* ... */
let selectAuthVC = /* ... */
selectAuthVM.didSendEventClosure = /* ... */
navigationController.pushViewController(selectAuthVC, animated: false)
}
// present로 EmailLoginView (추가적인 nav가 필요 없기 때문에 여기서 present 실행)
func showEmailLoginView() {
let emailLoginVM = /* ... */
let emailLoginVC = /* ... */
emailLoginVM.didSendEventClosure = /* ... */
// nav는 화면에 보이는 것 외에 다른 기능은 없다
let nav = UINavigationController(rootViewController: emailLoginVC)
navigationController.present(nav, animated: true)
}
}
내가 참고한 코디네이터 레퍼런스에서는
코디네이터가 finish로 종료되면,
부모 코디에서 그 코디의 타입에 따라 다음 코디를 정하고, 이를 실행시켰다.
하지만 하나의 코디가 종료될 때 상태에 따라 서로 다른 코디가 실행될 가능성이 있기 때문에 추가적인 코드의 필요성을 느꼈다.
finish
메서드는 extension Coordinator
에서 미리 선언이 되어있다.이 때, 매개변수로 넣는 타겟 코디는
타겟코디의_부모코디.ChildCoordinatorType.타겟코디 형식으로 넣어준다.
모든 부모코디는 ChildCoordinatorType
으로 본인의 자식 코디 종류를 저장하고 있다.
extension TabBarCoordinator {
enum ChildCoordinatorType: ChildCoordinatorTypeProtocol {
case homeDefaultScene(workspaceId: Int)
case dmScene(workspaceID: Int)
case searchScene, settingScene
}
즉,다음 타겟 코디를 넘겨줄 때는, 그 부모 코디가 누구인지도 알아야 한다.
self?.finish(
TabBarCoordinator.ChildCoordinatorType.homeDefaultScene(workSpaceId: workSpaceId)
)
모든 코디의 finish
메서드는 동일
extension Coordinator {
func finish(_ nextFlow: ChildCoordinatorTypeProtocol?) {
// 1. 자식 코디 다 지우기
childCoordinators.removeAll()
// 2. 부모 코디에게 알리기
finishDelegate?.coordinatorDidFinish(
childCoordinator: self,
nextFlow: nextFlow // 다음 타겟 코디
)
}
}
childCoordinators = childCoordinators.filter { $0.type != childCoordinator.type }
didFinish
가 실행될 것이고, 다음 타겟 코디를 실행시켜주어야 한다. 1. 타겟 코디가 없는 경우
if nextFlow == nil { }
2. Child 코디인 경우
else if nextFlow = nextFlow as? ChildCoordinatorType {
/* ... */
}
3. Child 코디보다 더 하위 코디인 경우
ex). 탭바코디의 자식코디
else if let nextFlow = nextFlow as? TabBarCoordinator.ChildCoordinatorType {
/* ... */
}
4. Parent 코디를 타고 가야 하는 경우
else {
self.finish(nextFlow)
}
enum Event
가 존재한다.// HomeDefaultVM
extension HomeDefaultViewModel {
enum Event {
case presentWorkSpaceListView(workSpaceId: Int)
case presentInviteMemberView
case presentMakeChannelView
case goExploreChannelFlow
case goChannelChatting(workSpaceId: Int, channelId: Int, channelName: String)
case goBackOnboarding
}
}
// HomeDefaultCoordinator - showHomeDefaultView
class HomeDefaultSceneCoordinator: HomeDefaultSceneCoordinatorProtocol {
/* ... */
func showHomeDefaultView(_ workSpaceId: Int) {
let homeDefaultVM = /* ... */
homeDefaultVM.didSendEventClosure = { [weak self] event in
switch event {
case .presentWorkSpaceListView(let workSpaceId):
self?.showWorkSpaceListFlow(workSpaceId: workSpaceId)
case .presentInviteMemberView:
self?.showInviteMemberView()
case .presentMakeChannelView:
self?.showMakeChannelView()
case .goExploreChannelFlow:
self?.showExploreChannelFlow(workSpaceId: (self?.workSpaceId)!)
case .goChannelChatting(let workSpaceId, let channelId, let channelName):
self?.showChannelChattingView(
workSpaceId: workSpaceId,
channelId: channelId,
channelName: channelName
)
case .goBackOnboarding:
self?.finish(AppCoordinator.ChildCoordinatorType.loginScene)
}
}
let vc = HomeDefaultViewController.create(with: homeDefaultVM)
navigationController.pushViewController(vc, animated: true)
}
}
기존에는 VC에서 화면 전환을 담당했기 때문에
VM에서 특정 시점이 되면 VC로 결과를 알려주어야 했다. (Output)
코디네이터를 쓴 이후에는 이 과정 자체가 필요가 없다. VC는 더 이상 화면 전환을 담당하지 않기 때문에 VM에서는 VC에게 시점을 알리지 않고, 바로 클로저를 통해 코디에게 시점을 알려준다.
이러면 하나의 코디네이터가 거의 하나의 View만 관리하게 되기 때문에 굳이 코디네이터를 쓸 필요가 없다고 생각했다.
물론 화면 전환에 대한 로직을 분리시킬 수는 있지만, 이 정도를 위해 매번 코디네이터를 새로 만드는 건 리소스 낭비라고 생각이 든다.