프로젝트 Github Repository: quokkaKyu/UIViewControllerCoordinator
iOS앱의 화면이 많아져서 화면을 전환하는 코드를 유지보수하는데 어려움과 불편함을 느꼈었습니다. 그래서 화면전환을 하는 책임을 다른 객체에 양도하고 싶어서 찾아본 결과 Coordinator패턴을 찾았습니다. 해당 글에 Coordinator 패턴을 적용한 내용을 기록하려고합니다.
Coordinator는 하나 이상의 뷰 컨트롤러를 관리하는 객체입니다. 뷰컨트롤러에서 모든 구동 로직을 제거하고, 해당 항목을 한 레이어 위로 이동하게하여 코드 유지보수에 도움을 줍니다.
모든것은 App Coordinator로부터 시작됩니다. App Coordinator는 앱의 기본 ViewControler를 설정합니다. App Coordinator는 ViewController를 생성 및 구성하거나 하위작업을 수행하기 위해 새로운 하위 코디네이터를 생성할 수 있습니다.
주로 탐색 및 모델 돌연변이입니다. (모델 돌연변이란 사용자의 변경 사항을 데이터베이스에 저장하거나 API에 PUT 또는 POST 요청을 하는 등 사용자의 데이터를 파괴적으로 수정할 수 있는 모든것을 의미합니다.
Coordinator에 대한 간략한 설명은 끝났습니다. Coordinator패턴에 대해 더 많은 설명이 필요하시다면 Coordinator Redux를 참고해주세요. 이제 코드에 적용을 해보겠습니다. 프로젝트의 미니멈 타겟은 iOS14입니다.
UIViewController의 화면전환에 대한 책임을 맡을 Coordinator protocol을 정의해줍니다.
protocol Coordinator: AnyObject {
var childCoordinators: [Coordinator] { get set } // 하위 Coordinator를 저장할 Coordinator 배열입니다.
var navigationController: UINavigationController { get set } // Coordinator에서 UIViewController의 화면전환에 대한 관리를 위해 선언해줍니다.
var rootVC: UIViewController? { get } // navigationController.pop을 실행할때 해당 Coordinator의 rootViewController까지만 pop을 진행하기 위해서 선언했습니다.
func start() // Coordinator를 시작합니다.
}
extension Coordinator {
func pop(animated: Bool = true) {
if navigationController.viewControllers.count > 1 {
navigationController.popViewController(animated: animated)
}
}
func popToRootVC(animated: Bool = true) {
guard let rootVC = rootVC else {
return
}
navigationController.popToViewController(rootVC, animated: animated)
}
}
AppCoordinator의 자식인 LoginCoordinator, MainCoordinator의 생명주기를 관리하고, isLoggedIn값에 따라서 로그인화면, 메인화면 둘중 어떤것을 보여줄지 결정합니다.
import Foundation
import UIKit
final class AppCoordinator: Coordinator, LoginCoordinatorDelegate, MainCoordinatorDelegate {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
var rootVC: UIViewController?
var isLoggedIn: Bool = false
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
if isLoggedIn {
showMainViewController()
} else {
showLoginViewController()
}
}
private func showLoginViewController() {
let coordinator = LoginCoordinator(navigationController: navigationController)
coordinator.delegate = self
coordinator.start()
childCoordinators.append(coordinator)
}
private func showMainViewController() {
let coordinator = MainCoordinator(navigationController: navigationController)
coordinator.delegate = self
coordinator.start()
childCoordinators.append(coordinator)
}
// LoginCoordinatorDelegate
func didLoggedIn(_ coordinator: LoginCoordinator) {
childCoordinators = childCoordinators.filter{ $0 !== coordinator}
showMainViewController()
}
// MainCoordinatorDelegate
func didLoggedOut(_ coordinator: MainCoordinator) {
childCoordinators = childCoordinators.filter { $0 !== coordinator }
showLoginViewController()
}
}
Login관련 뷰의 화면전환을 관리합니다.
import Foundation
import UIKit
protocol LoginCoordinatorDelegate: AnyObject {
func didLoggedIn(_ coordinator: LoginCoordinator)
}
final class LoginCoordinator: Coordinator {
var rootVC: UIViewController?
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
var delegate: LoginCoordinatorDelegate?
init(navigationController: UINavigationController!) {
self.navigationController = navigationController
}
func start() {
let viewController = LoginViewController()
viewController.coordinator = self
rootVC = viewController
navigationController.viewControllers = [viewController]
}
// LoginDestination의 case에 있는 뷰로 이동할 수 있습니다.
func push(destination: LoginDestination) {
switch destination {
case .signup:
let signupViewController = SignupViewController()
signupViewController.coordinator = self
navigationController.pushViewController(signupViewController, animated: false)
case .findID:
let findIDViewController = FindIDViewController()
findIDViewController.coordinator = self
navigationController.pushViewController(findIDViewController, animated: false)
}
}
// 해당 함수를 이용하여 LoginCoordinatorDelegate에 현재 Coordinator 인스턴스를 전달해줍니다.
func login() {
delegate?.didLoggedIn(self)
}
}
로그인 관련 이동할 화면을 정의합니다.
import Foundation
// 이동할 뷰
enum LoginDestination {
case signup
case findID
}
Main관련 뷰의 화면전환을 관리합니다.
import Foundation
import UIKit
protocol MainCoordinatorDelegate: AnyObject {
func didLoggedOut(_ coordinator: MainCoordinator)
}
final class MainCoordinator: Coordinator {
var rootVC: UIViewController?
var childCoordinators: [Coordinator] = []
var delegate: MainCoordinatorDelegate?
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let viewController = MainViewController()
viewController.coordinator = self
rootVC = viewController
navigationController.viewControllers = [viewController]
}
// 해당 함수를 이용하여 MainCoordinatorDelegate 현재 Coordinator 인스턴스를 전달해줍니다.
func logout() {
delegate?.didLoggedOut(self)
}
}
앱화면의 rootViewController를 navigationController로 설정하고 설정한 navigationController를 AppCoordinator에 초기화 해주고 AppCoordinator를 시작합니다.
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
let navigationController = UINavigationController()
window?.rootViewController = navigationController
let coordinator = AppCoordinator(navigationController: navigationController)
coordinator.start()
window?.makeKeyAndVisible()
}
}
MainViewController에 MainCoordinator를 이용한 화면전환 코드를 작성합니다.
final class MainViewController: UIViewController {
weak var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
private func setupView() {
view.backgroundColor = .gray
setupNavigationItem()
setupLabel()
}
private func setupNavigationItem() {
let item = UIBarButtonItem(title: "로그아웃", style: .plain, target: self, action: #selector(logoutButtonDidTap))
navigationItem.rightBarButtonItem = item
}
private func setupLabel() {
let label = UILabel()
label.text = "메인"
label.font = .systemFont(ofSize: 20, weight: .bold)
label.textAlignment = .center
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
@objc private func logoutButtonDidTap() {
coordinator?.logout()
}
LoginViewController에 LoginCoordinator를 이용한 화면전환 코드를 작성합니다.
import Foundation
import UIKit
final class LoginViewController: UIViewController {
weak var coordinator: LoginCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
private func setupView() {
view.backgroundColor = .white
setupNavigationItem()
setupLabel()
}
private func setupNavigationItem() {
let loginButtonitem = UIBarButtonItem(title: "로그인", style: .plain, target: self, action: #selector(loginButtonDidTap))
let findIDButtonItem = UIBarButtonItem(title: "아이디찾기", style: .plain, target: self, action: #selector(findIDButtonDidTap))
let signupButtonItem = UIBarButtonItem(title: "회원가입", style: .plain, target: self, action: #selector(signupButtonDidTap))
navigationItem.rightBarButtonItems = [loginButtonitem, findIDButtonItem, signupButtonItem]
}
private func setupLabel() {
let label = UILabel()
label.text = "로그인"
label.font = .systemFont(ofSize: 20, weight: .bold)
label.textAlignment = .center
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
@objc private func loginButtonDidTap() {
coordinator?.login()
}
@objc private func findIDButtonDidTap() {
coordinator?.push(destination: .findID)
}
@objc private func signupButtonDidTap() {
coordinator?.push(destination: .signup)
}
}
SignupViewController, FindIDViewController는 위에 뷰컨트롤러 코드를 이용하여 응용하시면 될거같고, 전체 소스코드는 맨위에 남긴 github주소에서 참고하시면 됩니다. 피드백은 언제든지 환영입니다! 읽어주셔서 감사합니다!