- MVVMC 는 기존 MVVM 아키텍처 패턴에 Coordinator을 추가한 조합
- MVVMC, MVVM-C, MVVM Coordinator 패턴이라고도 함
- MVVM에서 화면 전환(navigation) 기능을 Coordinator가 맡아 처리함
MVVM 이 처음이라면 👇
2022.06.07 - [🍎 iOS/Architecture Pattern] - [Swift] MVVM 응용편
MVVM+C 를 구현하는 방법이 딱 정해져 있는 것이 아니라
Coordinator 역활 안에서 구현하는 방법이 다양하게 존재하더라구요
생각보다 복잡한 부분들이 많아서 이번 포스팅에서는 간단하게 살펴 보겠습니다!
우선 Coordinator의 역활 이해하기!
MVVM+C 살펴보기
⭐️ Model
- 데이터 구조 정의
- 애플리케이션에서 필요한 데이터 정의
- struct 혹은 아주 간단한 class 로 구현함
⭐️ View
- view 코드는 흔히 ViewController에 작성됨
- 사용자의 상호작용을 담당
- 이벤트가 발생하면 ViewModel에게 알려줌
- UI를 표시하기 위한 로직만 담당
⭐️ ViewModel
- View 로부터 사용자의 상호작용을 받아 이에 맞는 이벤트를 처리함
- 비즈니스 로직을 구현
- UIKit을 필요로 하지 않음
- model의 정보를 view에 보여주기 위한 값으로 변경 (ViewData를 사용해서 한번더 Model을 View에서 최종적으로 사용할 값으로 처리하는 방법도 있음)
⭐️ Coordinator
- navigation을 담당함 (화면 이동 - 쉽게 말해 어떤 화면이 보여져야 하며 다음에는 이 화면이 보여져야 한다를 정의)
- ViewController에서 필요로 하는 dependencies를 생성하고 inject하는 역활
- 화면 이동에 대한 단일 책임 원칙을 따르게 됨 (ViewController는 화면 이동에 대해서 이 다음에 어떤 화면이 와야 하고 어떤 dependencies를 전달해야 하는지 알지 않아도 됨)
튜토리얼
👻 Coordinator 프로토콜 생성하기
protocol Coordinator {
var children: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
}
Coordinator.swift
Coordinator를 구현하는 방법은 아주 다양하더라구요!
protocol로 생성하시는 분도 있고 간단한 클래스로 작성하시는 분도 있는데 여기서는 protocol로 간단하게 해보겠습니다
Coordinator을 시작할 때 해주어야 할 작업이 start()에 들어가게 됩니다
Coordinator가 끝날 때 필요한 작업도 있는데 그건 다음 포스팅에..🥲
var children: [Coordinator] { get set }
Coordinator는 화면 이동과 ViewController에 대한 정보를 가지고(own) 있습니다.
이때 하나의 Coordinator가 앱의 모든 화면 이동을 처리하지 않고
Scene 별로 child coordinator을 구현해 주겠습니다
예를 들어 Scene은 로그인에 대한 플로우를 하나의 씬으로 해서
로그인에 필요한 모든 화면이동을 처리하는 Coordinator을 child coordinator로 생성하여 사용합니다
이렇게 하나의 플로우를 하나의 Coordinator로 묶어도 되겠지만
연관이 있는 화면들을 묶어서 하나의 coordinator로 관리해도 좋을거 같습니다
👻 AppCoordinator 생성하기
import UIKit
class AppCoordinator: Coordinator {
var children = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
}
}
AppCoordinator.swift
AppCoordinator는 Coordinator 프로토콜을 따르는 가장 최상위 Coordinator 입니다
navigation을 할 navigationController를 initializer에서 받습니다
👻 window rootViewController 설정하기
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var appCoordinator: AppCoordinator? // ⭐️
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
let navigationController = UINavigationController()
window?.rootViewController = UserViewController()
window?.makeKeyAndVisible()
self.appCoordinator = AppCoordinator(navigationController: navigationController)
self.appCoordinator?.start()
}
}
SceneDelegate.swift
appCoordinator 프로퍼티를 추가해 주세요
window 의 rootViewController가 될 UINavigationController를 생성하여 설정해 줍니다
그리고 이 navigationController를 appCoordinator의 dependency로 넘겨줍니다
마지막으로 appCoordinator의 start 함수를 호출하면
SceneDelegate 작성이 끝납니당
👻 ViewModel 생성하기
import Foundation
class LandingViewModel {
weak var coordinator: AppCoordinator?
}
LandingViewModel.swift
LandingViewModel은 AppCoordinator에 접근할 수 있는 프로퍼티인
coordinator을 가지고 있습니다
사용자가 '로그인'이라는 버튼을 누르면 View는
해당 이벤트를 처리하는 ViewModel에게 이벤트가 일어 났음을 알립니다
ViewModel은 화면 전환 처리를 담당하는 Coordinator에게 전달하게 되고
Coordinator는
Login 화면을 화면에 보여주도록 처리합니다
(화면 전환에 관련된 로직이기 때문에
view에서 바로 coordinator에게 이벤트 처리를 담당하게 할 수도 있습니다)
👻 ViewController 생성하기
import UIKit
class LandingViewController: UIViewController {
var viewModel: LandingViewModel?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
attachUI()
}
func attachUI() {
var config = UIButton.Configuration.filled()
config.baseBackgroundColor = .blue
let loginButton = UIButton(type: .system)
view.addSubview(loginButton)
loginButton.frame = CGRect(x: 20, y: view.center.y, width: UIScreen.main.bounds.width - 40, height: 40)
loginButton.setTitle("로그인", for: .normal)
loginButton.configuration = config
loginButton.addAction(UIAction { _ in
self.goToLoginPage()
}, for: .touchUpInside)
}
func goToLoginPage() {
}
}
LandingViewController.swift
Landing 화면에서 로그인 버튼을 누르면
Login 화면으로 이동합니다
loginButton을 누르게 되면 goToLoginPage() 가 호출됩니다
이 이벤트를 ViewModel에게 알려줍시당
func goToLoginPage() {
viewModel?.goToLoginPage()
}
LandingViewController.swift
LandingViewController의 goToLoginPage() 에서 viewModel의 goToLoginPage() 함수를 호출해 주면
화면전환 이벤트 처리가 발생했다는 것을 viewModel에게 알리게 되는 것!
이때 에러가 뜰텐데 아직 viewModel에 해당 함수를 구현하지 않아서 그렇습니다!!
다시 viewModel로 이동해 줍시다
class LandingViewModel {
weak var coordinator: AppCoordinator?
func goToLoginPage() {
coordinator?.goToLoginPage()
}
}
LandingViewModel.swift
viewModel은 화면 전환 처리이니
이를 coordinator에게 알립니다
coordinator에 아직 구현하지 않았지만
goToLoginPage()를 호출합니다
coordinator의 goToLoginPage()는 현재 화면인 LandingViewController 에서
LoginViewController를 push해 주는 화면전환 처리를 하게 됩니다
👻 AppCoordinator에서 화면 전환 처리하기
우선 start() 함수를 작성하겠습니다
SceneDelegate에서 AppCoordinator의 start 함수를 호출하게 됩니다
이때 window의 rootViewController는 설정해 주었던 navigationController입니다
이 navigationController는 AppCoordinator를 생성할 때 인자로 받은 값입니다
func start() {
let landingViewModel = LandingViewModel()
landingViewModel.coordinator = self
let landingViewController = LandingViewController()
landingViewController.viewModel = landingViewModel
navigationController.pushViewController(landingViewController, animated: false)
}
AppCoordinator.swift
의존관계를 설정해 줍니다
viewModel의 coordinator를 AppCoordinator 로
viewController의 viewModel도 설정해 줍니다
그리고 navigationController에 landingViewController를 push 해주세요
sceneDelegate에 의해 start()가 호출되면 해당 로직이 실행되면서 화면에 LandingViewController 가 보이게 될거에요!
이제 goToLoginPage() 함수를 추가해 줍니다
func goToLoginPage() {
print("move to login page !!!")
}
사용자가 로그인 버튼 누름 -> view가 viewModel에게 이벤트를 알림 -> viewModel이 coordinator에게 화면 전환 처리를 하도록 요청
여기까지 작성하시고 실행을 하면 콘솔에 위 프린트 문이 찍히는 것을 확인할 수 있습니다!
LoginViewController로 이동하기 위해서
LoginViewController와 LoginViewModel을 생성해 주겠습니다
import Foundation
class LoginViewModel {
weak var coordinator: AppCoordinator?
}
LoginViewModel.swift
import UIKit
class LoginViewController: UIViewController {
var viewModel: LoginViewModel?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .orange
}
}
LoginViewController.swift
다시 AppCoordinator로 돌아와서 goToLoginPage()를 완성해 줍시당
func goToLoginPage() {
let loginViewModel = LoginViewModel()
loginViewModel.coordinator = self
let loginViewController = LoginViewController()
loginViewController.viewModel = loginViewModel
navigationController.pushViewController(loginViewController, animated: true)
}
AppCoordinator.swift
coordinator에서 LandingViewController -> LoginViewController로 이동하게 됩니다
👉🏻 LandingViewController
사용자의 이벤트를 받아 viewModel에게 알림
👉🏻 LandingViewModel
비즈니스 로직 처리
coordinator에게 화면전환 처리 알림
👉🏻 AppCoordinator
화면 전환 처리
유즈케이스, 혹은 플로우에 따라 childCoordinator을 만들어 주고
parentCoordinator의 childCoordinator로 설정해서
복잡한 화면 전환 처리를 담당할 수 있습니다
viewController는 화면전환을 담당하지 않아 coordinator을 사용해서
단일 책임 원칙을 따를 수 있고
어떤 화면이 다음에 나오거나 어디로 이동해야 하는지에 대해 알지 않아도 됩니다
다음 포스팅에서는 childCoordinator을 관리하는 로직을
알아보겠습니당
혹시 틀린점이나 이상한 점이 있다면 댓글로 알려주시면 감사하겠습니다!! 🦑
'🍎 iOS' 카테고리의 다른 글
[Swift] MVVMC의 Coordinator 알아보기 (0) | 2022.06.14 |
---|---|
[Swift] UITextField borderStyle 텍스트 필드 테두리 설정 (0) | 2022.06.13 |
[Swift] MVVM 응용편 (0) | 2022.06.07 |
[iOS/Swift] UICollectionView scroll animation (0) | 2022.06.06 |
[iOS/Swift] UICollectionViewFlowLayout 알아보기 (0) | 2022.06.06 |
댓글