UIKit / Coordinator Pattern

iOS 앱개발 공부

목록 보기
6/30
post-thumbnail

🧠 핵심 요약

Coordinator 패턴은 iOS 애플리케이션의 화면 전환과 화면 흐름을 관리하기 위한 디자인 패턴이다.
일반적으로 UIViewController가 화면 이동과 관련된 로직을 직접 담당하지만, 이 경우 UIViewController가 너무 많은 역할을 맡게 되어 특히 MVC 구조에서 ViewController가 비대해지는 문제가 발생할 수 있다.

이러한 문제를 해결하기 위해 Coordinator 패턴화면 전환 로직을 별도의 객체로 분리하여 관리한다.
즉, UIViewController는 화면 UI와 사용자 인터랙션에 집중하고, 화면 흐름과 관련된 로직은 Coordinator라는 객체가 담당하는 방식이다.

✅ 장점

  • UIViewController가 화면 흐름을 직접 관리하지 않기 때문에 ViewController의 역할이 단순해지고 코드의 유지보수성이 향상
  • 특정 화면의 흐름을 변경하거나 같은 Coordinator 객체를 여러 곳에서 재사용할 수 있어 재사용성과 유연성이 증가
  • ViewController가 Coordinator에 의존하므로, 화면 흐름을 테스트할 때 독립적인 테스트가 가능하여 테스트 용이성이 향상

🧩 구조도

✔️ 핵심 코드

Coordinator Protocol

protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set } // 자식 코디네이터 보관 -> 강한 참조
    var navigationController: UINavigationController { get set }
    func start()
}

Coordinator Delegate

protocol MainCoordinatorDelegate: AnyObject {
    func mainCoordinatorDidRequestDetail(_ coordinator: MainCoordinator, with item: CalculateModel) // Detail 뷰로 push
    func mainCoordinatorDidFinish(_ coordinator: MainCoordinator) // 코디네이터 종료 알림
}

⭐️ 실습하기

Coordinator 패턴을 익히기 위해, 간단히 만들어진 환율 어플에서 환율 계산기 화면으로 이동하는 것을 만들어 보려고 한다.

MainView -> push -> CalculateView -> pop -> MainView
MainViewCalculateView

1) AppCoordinator 정의하기

먼저 전체 Coordinator를 관리할 AppCoorinator를 구현해준다.

// Coordinator Protocol
protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get set }
    func start()
}

// AppCoordinator
final class AppCoordinator: Coordinator {
    
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
    	// 추후 구현
    }

}

AppCoordinator는 앱 전체의 화면 흐름을 관리하는 메인 코디네이터이다.
테스트를 위한 앱에서는 MainViewCalculateView 두 가지의 화면이 있기 때문에, 각 화면별로 하나의 코디네이터를 구현해주어야 한다.

2) 화면별 Coordinator 구현

먼저 메인 화면의 코디네이터를 구현해보자

// 메인 화면 코디네이터
final class MainCoordinator: Coordinator {
    
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        let vc = MainViewController()
        vc.coordinator = self // 메인 뷰에 weak var coordinator 구현 후 정의
        navigationController.viewControllers = [vc]
    }
}
// 환율 계산기 화면 코디네이터
final class DetailCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
            
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let vc = CalculateViewController()
        vc.coordinator = self // 환율 계산기 뷰에 weak var coordinator 구현 후 정의
        navigationController.pushViewController(vc, animated: true)
    }
}

이렇게 하면 기본적인 구현이 완료되어, 각 코디네이터에서 start() 메서드를 사용하면 화면을 불러올 수 있게된다.
아직 구현이 안된 것이 있는데, 메인 화면에서 계산기 화면으로 넘어가기 위해서는 push 요청을 했다는 것을 AppCoordinator가 알 수 있어야 한다. 이를 위해 Delegate를 구현해 주어야 한다.

// 메인 화면 Delegate
protocol MainCoordinatorDelegate: AnyObject {
    func mainCoordinatorDidRequestDetail(_ coordinator: MainCoordinator, with item: CalculateModel) // push 요청
    func mainCoordinatorDidFinish(_ coordinator: MainCoordinator) // 코디네이터 명시적 제거
}

// 환율 계산기 화면 Delegate
protocol DetailCoordinatorDelegate: AnyObject {
    func detailCoordinatorDidFinish(_ coordinator: DetailCoordinator) // 코디네이터 명시적 제거
}

그리고 이렇게 만들어진 Delegate를 AppCoordinator에 채택한다.

extension AppCoordinator: MainCoordinatorDelegate {
    
    func mainCoordinatorDidRequestDetail(_ coordinator: MainCoordinator, with item: CalculateModel) {
        let detailCoordinator = DetailCoordinator(navigationController: navigationController, item: item)
        detailCoordinator.delegate = self
        childCoordinators.append(detailCoordinator) // 자식 코디네이터를 강하게 참조
        detailCoordinator.start()
    }
    
    func mainCoordinatorDidFinish(_ coordinator: MainCoordinator) {
        childCoordinators = childCoordinators.filter { $0 !== coordinator }
    }
    
}

extension AppCoordinator: DetailCoordinatorDelegate {
    
    func detailCoordinatorDidFinish(_ coordinator: DetailCoordinator) {
        childCoordinators = childCoordinators.filter { $0 !== coordinator }
    }
    
}

이제 각 코디네이터에 delegate를 만들어주고, 각 ViewController에서는 상황에 맞게 메서드를 호출하도록 하면 된다.

final class MainCoordinator: Coordinator {
	weak var delegate: MainCoordinatorDelegate?
    
    // ...
    
    func showDetail(for item: CalculateModel) {
        delegate?.mainCoordinatorDidRequestDetail(self, with: item)
    }
    
    func finishMainFlow() {
        delegate?.mainCoordinatorDidFinish(self)
    }
}

final class DetailCoordinator: Coordinator {
	weak var delegate: DetailCoordinatorDelegate?
    
    // ...
    
    func detailCoordinatorDidFinish() {
        delegate?.detailCoordinatorDidFinish(self)
    }
}

앞서 AppCoordinator에서 구현하지 않은 start() 메서드도 구현해준다.

func start() {
	let mainCoordinator = MainCoordinator(navigationController: self.navigationController)
	mainCoordinator.delegate = self
	childCoordinators.append(mainCoordinator) // 자식 코디네이터를 강하게 참조
	mainCoordinator.start()
}

이제 코디네이터를 사용할 준비는 완료되었으니, AppDelegate에서 코디네이터를 사용하여 화면을 불러오면 된다.

3) AppDelegate 설정하기

AppDelegate에 코디네이터를 구현할 때 중요한 것은, AppCoordinator가 메모리에서 해제되면 안된다는 것이다.
이를 위해 AppCoordinator를 지역 변수가 아닌 프로퍼티로 정의하는 것이 좋다.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    var appCoordinator: AppCoordinator? // 프로퍼티 선언

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = (scene as? UIWindowScene) else { return }
        let navigationController = UINavigationController()
        window = UIWindow(windowScene: scene)
        window?.rootViewController = navigationController
        window?.makeKeyAndVisible()
        
        appCoordinator = AppCoordinator(navigationController: navigationController)
        appCoordinator?.start() // 화면 구현
    }
    
    // ...
}

4) 구현 결과


📌 결론

화면의 흐름을 구현할 때, UINavigationController를 사용한다는 것에 큰 의문을 가지지 않았었는데,
Coordinator 패턴을 공부하면서 문제점을 인지하게 되었고, 왜 Coordinator 패턴이 생겼는지 알게 되었다.

앞으로는 앱을 구현할 때 화면 흐름에 대해서도 더 잘 고려를 해봐야겠다고 느꼈다.

profile
이유있는 코드를 쓰자!!

0개의 댓글