ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [iOS-1] Coordinator 패턴을 사용한 화면 전환 관리
    Project/TalkTalk 2025. 3. 28. 22:03

    Coordinator 패턴이란?

    Coordinator 패턴은 화면 전환 로직을 ViewController로부터 분리하여 별도의 객체(Coordinator)에게 위임하는 아키텍처 패턴입니다. 이 패턴을 통해 ViewController는 UI 표시와 사용자 입력 처리에만 집중할 수 있고, 복잡한 화면 전환과 앱의 전체 흐름은 Coordinator가 관리하게 됩니다.

    Coordinator 패턴 구현 

    Coordinator 프로토콜을 정의하여 각 화면을 관리하는 Coordinator들이 반드시 포함해야할 구현을 강제합니다. 

    /// Coordinator 패턴을 구현하기 위한 기본 프로토콜
    /// 앱의 화면 흐름을 관리하고 뷰 컨트롤러 간 전환 로직을 캡슐화합니다.
    protocol Coordinator: AnyObject {
        /// 현재 Coordinator가 관리하는 자식 Coordinator들의 배열
        /// 계층적 흐름 관리와 메모리 관리를 위해 사용됩니다.
        var childCoordinators: [Coordinator] { get set }
        
        /// Coordinator가 화면 전환에 사용하는 네비게이션 컨트롤러
        /// 뷰 컨트롤러 푸시, 팝 등의 전환을 담당합니다.
        var navigationController: UINavigationController { get set }
        
        /// Coordinator의 화면 흐름을 시작하는 메서드
        /// 일반적으로 첫 번째 뷰 컨트롤러를 네비게이션 스택에 추가합니다.
        func start()
    }

     

    각 계층의 화면 Flow를 관리하는 Coordinator들은 위에서 정의한 프로토콜을 채택하고 구현함으로써 화면 계층과 화면간 전환에 대한 실제 방법을 구현합니다. 

    /// 앱의 최상위 Coordinator
    /// 각 화면은 담당 Coordinator들이 관리하고 여기서는 Coordinator들을 관리
    class AppCoordinator: Coordinator {
        var childCoordinators: [Coordinator] = []
        var navigationController: UINavigationController
        private let window: UIWindow
        
        func start() {
            window.rootViewController = navigationController
            window.makeKeyAndVisible()
            
            // 로그인 상태에 따라 적절한 flow 보여주기
            if isUserLoggedIn() {
                showMainFlow()
            } else {
                showAuthFlow()
            }
        }
    	...
    }
    
    /// 인증 흐름 관리
    class AuthCoordinator: Coordinator {
        var childCoordinators: [Coordinator] = []
        var navigationController: UINavigationController
        weak var delegate: AuthCoordinatorDelegate?
        
        init(navigationController: UINavigationController) {
            self.navigationController = navigationController
        }
        
        func start() {
            let loginVC = LoginViewController()
            loginVC.coordinator = self
            // 애니메이션과 함께 stack 교체
            navigationController.setViewControllers([loginVC], animated: true)
        }
        
        func goToRegister() {
            let registerVC = RegisterViewController()
            registerVC.coordinator = self
            navigationController.pushViewController(registerVC, animated: true)
        }
        
        func didFinishLogin() {
            delegate?.didFinishAuth(self)
        }
        
        func didFinishRegister() {
            delegate?.didFinishAuth(self)
        }
    }

     

    Coordinator 패턴에서는 ViewController는 화면 전환에 대한 책임을 모두 Coordinator에게 전가합니다. 따라서 CoordinatedViewController라는 프로토콜을 정의하고 내부 프로퍼티로 coordinator를 두어서 모든 ViewController들이 coordinator를 통해 화면 전환을 하도록 강제합니다. 

    // 모든 화면에 공통으로 적용되는 기본 프로토콜
    protocol CoordinatedViewController: UIViewController {
        associatedtype CoordinatorType: Coordinator
        var coordinator: CoordinatorType? { get set }
    }

     

    각 ViewController에 요구되는 화면 전환 종류를 명시하는 프로토콜을 정의하고 여기에 CoordinatedViewController를 채택과 동시에 where절을 통해 사용되어야할 구체 coordinator 타입을 명시합니다. 그리고 ViewController에게 이 프로토콜을 채택 시킴으로써 화면 전환을 모두 coordinator에게 넘기도록 합니다. 

    // 인증 화면 관련 프로토콜
    protocol LoginViewControllerProtocol: CoordinatedViewController where CoordinatorType == AuthCoordinator {
        func didTapLoginButton()
        func didTapRegisterButton()
    }
    
    class LoginViewController: UIViewController, LoginViewControllerProtocol {
        weak var coordinator: AuthCoordinator?

     

    화면 전환 방식

    TalkTalk 앱의 Coordinator 계층 구조와 화면 전환 방식을 시각화한 구조도입니다.

    1. 부모-자식 관계 (Parent-Child):  상위 Coordinator가 하위 Coordinator를 생성하고 관리하는 계층적 구조입니다.

    AppCoordinator (최상위)
    ├── AuthCoordinator
    └── MainCoordinator
        ├── ChatCoordinator
        └── ProfileCoordinator
    /// 앱의 최상위 Coordinator
    /// 각 화면은 담당 Coordinator들이 관리하고 여기서는 Coordinator들을 관리
    class AppCoordinator: Coordinator {
    	...
        func start() {
            window.rootViewController = navigationController
            window.makeKeyAndVisible()
            
            // 로그인 상태에 따라 적절한 flow 보여주기
            if isUserLoggedIn() {
                showMainFlow()
            } else {
                showAuthFlow()
            }
        }
    	...
        private func showAuthFlow() {
            let authCoordinator = AuthCoordinator(navigationController: navigationController)
            authCoordinator.delegate = self
            childCoordinators.append(authCoordinator)
            authCoordinator.start()
        }
        
        private func showMainFlow() {
            let mainCoordinator = MainCoordinator(navigationController: navigationController)
            childCoordinators.append(mainCoordinator)
            mainCoordinator.start()
        }
    }

    AppCoordinator는 로그인 상태에 맞는 하위 coordinator를 생성하고 start( ) 메서드를 호출하면서 적절한 화면으로 이동합니다. 

     

    2. 위임 (Delegation): 주로 하위 Coordinator가 이벤트를 상위 Coordinator에게 알릴 때 사용합니다.

    class AppCoordinator: Coordinator {
    	...
        init(window: UIWindow) {
            self.window = window
            self.navigationController = UINavigationController()
        }
        
    	...
        
        private func showMainFlow() {
            let mainCoordinator = MainCoordinator(navigationController: navigationController)
            childCoordinators.append(mainCoordinator)
            mainCoordinator.start()
        }
    }
    
    /// AuthCoordinatorDelegate 채택 및 구현으로 AuthCoordinator에서 이벤트 발생시 AppCoordinator에서 동작가능
    extension AppCoordinator: AuthCoordinatorDelegate {
        func didFinishAuth(_ coordinator: AuthCoordinator) {
            // Child coordinator에서 AuthCoordinator 제거 후 메인 화면으로 이동
            childCoordinators = childCoordinators.filter { $0 !== coordinator }
            showMainFlow()
        }
    }
    
    /// Delegate 패턴을 활용하여 상위, 하위 컴포넌트간의 이벤트 발신, 수신 
    protocol AuthCoordinatorDelegate: AnyObject {
        func didFinishAuth(_ coordinator: AuthCoordinator)
    }
    
    /// AuthCoordinator에서 이벤트 감지시 delegate(AppCoordinator)를 이용한 화면 전환 
    class AuthCoordinator: Coordinator {
    	...
        weak var delegate: AuthCoordinatorDelegate?
    
    	...
        
        func didFinishLogin() {
            delegate?.didFinishAuth(self)
        }
        
        func didFinishRegister() {
            delegate?.didFinishAuth(self)
        }
    }

    Delegate 패턴은 동작을 수신하는 컴포넌트와 실제 동작을 수행해야하는 컴포넌트가 분리되어 있을 때 사용하는 통신 방식입니다.

    위의 경우 회원 가입 완료 버튼을 관리하는 것은 AuthCoordinator인데, 회원 가입 완료 버튼이 눌렸을 때 메인 흐름으로 전환시키 위해서는 AppCoordinator가 동작해야합니다. 따라서 AuthCoordinatorDelegate를 정의한 다음 이를 AppCoordinator에 채택시킵니다. 이후 AppCoordinator에서 AuthCoordinator를 생성할 때 자기 자신을 delegate(대리자)로 설정함으로써 AuthCoordinator에서 동작이 일어났을 때 AppCoordinator의 자원(프로퍼티, 메서드)를 활용하여 이에 대한 구현을 제공할 수 있습니다. 

     

    3. 직접 참조 (Direct Reference): ViewController가 자신의 Coordinator 메서드를 직접 호출하는 방식입니다.

    class ProfileViewController: UIViewController, ProfileViewControllerProtocol {
        weak var coordinator: ProfileCoordinator?
    	...
        private let editProfileButton = UIButton(type: .system)
    	...
        
        override func viewDidLoad() {
            super.viewDidLoad()
            setupUI()
        }
        
        private func setupUI() {
            ...
            // 버튼 설정
            editProfileButton.addTarget(self, action: #selector(editProfileButtonTapped), for: .touchUpInside)
        }
        
        @objc private func editProfileButtonTapped() {
            didTapEditProfileButton()
        }
        ...
        
        func didTapEditProfileButton() {
            coordinator?.showEditProfile()
        }
    	...
    }

    editProfileButton에 대해 사용자 액션이 발생하면 addTarget을 통해 매핑된 @objc 메서드 내부에서 자신이 weak 참조하는 coordinator를 사용해 화면을 전환합니다. 

     

    4. 알림 (Notification):여러 컴포넌트에 동시에 알림이 필요한 전역 이벤트에 사용됩니다.

    // ProfileViewController에서 로그아웃 요청
    func didTapLogoutButton() {
        // 알림창 표시 후
        coordinator?.logout()
    }
    
    // ProfileCoordinator에서 로그아웃 처리
    func logout() {
        NotificationCenter.default.post(name: NSNotification.Name("LogoutNotification"), object: nil)
    }
    
    // SceneDelegate에서 로그아웃 알림 관찰
    private func setupLogoutObserver() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleLogout),
            name: NSNotification.Name("LogoutNotification"),
            object: nil
        )
    }
    
    @objc private func handleLogout() {
        if let window = self.window {
            appCoordinator = nil
            appCoordinator = AppCoordinator(window: window)
            appCoordinator?.start()
        }
    }

    로그아웃은 앱 전체에 영향을 미치는 이벤트이므로 NotificationCenter를 통해 처리됩니다. LogoutNotification은 프로필 화면에서 로그아웃 시 발생하며, SceneDelegate가 이를 감지하여 AppCoordinator를 재설정합니다.

    TalkTalk 앱의 전체 화면 흐름

    1. 앱 시작 시 SceneDelegate가 AppCoordinator를 생성하고 시작합니다.
    2. AppCoordinator는 로그인 상태에 따라 인증 흐름 또는 메인 앱 흐름을 시작합니다.
    3. 인증 흐름에서 사용자가 로그인 또는 회원가입을 완료하면 AuthCoordinator는 delegate를 통해 AppCoordinator에게 알립니다.
    4. AppCoordinator는 메인 앱 흐름을 시작하고, MainCoordinator가 탭 바 컨트롤러를 설정합니다.
    5. 메인 앱에서는 ChatCoordinator와 ProfileCoordinator가 각 탭의 화면 흐름을 관리합니다.
    6. 사용자가 로그아웃하면 알림이 발생하고, SceneDelegate가 이를 감지하여 AppCoordinator를 재설정합니다.

    'Project > TalkTalk' 카테고리의 다른 글

    [iOS-2] Dependency Container로 의존성 관리하기  (0) 2025.03.31

    댓글

Designed by Tistory.