Skip to content

화면 전환(Game NavigationController)

Tltlbo edited this page Dec 4, 2024 · 1 revision

각 플레이어 모두 동기화된 같은 화면을 바라보아야 한다.

저희는 세션을 통한 연결이 아니기 때문에 클라이언트 측에서 DB의 현재 속해 있는 방의 게임 상태 값을 보고 화면을 보여주어야 합니다.

그렇다면 화면을 이동하기 위해서는 각 ViewController가 DB의 게임 상태 값을 구독하고,

값에 따라 화면을 이동 시켜주어야 하는 상태가 됩니다.

이렇게 되면 각 VC들은 DB의 게임 상태를 bind하는 함수가 추가되어야 하고,

화면을 이동 시키기 위해 다음 화면에 필요한 인스턴스를 생성하고, 주입하고 끝으로 화면을 이동 시켜주어야 하는 책임이 발생하게 됩니다.

해당 방식은 꽤나 번거롭고 화면마다 다음 화면으로 넘어갈 작업을 작성해주어야 해 까다롭기까지 한 것 같습니다.

Coordinator 패턴

화면 전환에 있어 가장 유명한 패턴인 Coordinator 패턴이 있습니다.

해당 패턴은 VC 자체가 화면 전환에 필요한 인스턴스의 생성과 이동을 담당하지 않고 모두 Coordinator라는 객체를 통해서 해당 객체가 전담합니다.

protocol ProductDetailCoordinator: AnyObject {
  func pushToDetail(_ navigationController: UINavigationController, productId: String)
}

extension ProductDetailCoordinator {

  func pushToDetail(_ navigationController: UINavigationController, productId: String) {
    let vc = DetailViewController()
    vc.setNavigationTitle("상세화면")
    vc.productId = productId
    navigationController.pushViewController(vc, animated: true)
  }

}
class ListViewController: UIViewController {
  weak var coordinator: ProductDetailCoordinator?

  ...

  func productTapped(_ productId: String) {
    coordinator?.pushToDetail(navigationController, productId: String)
  }

  ...

}

이런 식으로 VC 측에서는 Coordinator의 메서드만 호출하고 그 어떠한 행동도 하지 않습니다.

Coordinator는 현재 NavigationController의 스택에 넣어줌으로써 화면 전환을 진행합니다.

근데 1가지 더 남았다. DB의 게임 상태 구독

저희 프로젝트는 게임이기 때문에 모든 플레이어가 같은 화면을 볼 필요가 있습니다.

그러므로 각 VC는 DB의 게임 상태 값을 구독하고 있고 게임 상태 값에 따라 Coordinator에게 화면 전환을 요청해야 합니다.

이렇게 되면 각 VC들은 게임 상태 값을 계속 구독해주고 있어야 하는 책임이 생깁니다. 저희는 이걸 막고 싶었습니다.

게임 상태에 따른 화면 전환을 책임지는 하나의 큰 Controller

일단은 게임 상태를 1곳에서만 구독하고 화면 전환을 맡아야겠다는 생각이 들어 하나의 Controller에서 화면의 전환을 맡아야겠다는 생각을 하게 되었습니다.

GameNavigationController라는 모든 화면 전환을 도맡는 Controller를 생성하고 내부에 navigationController를 두어 하나의 navigationController에서 화면 전환이 이루어지게 하였습니다.

final class GameNavigationController: @unchecked Sendable {
	private let navigationController: UINavigationController
	...
	private var gameInfo: GameState? {
        didSet {
            guard let gameInfo else { return }
            updateViewControllers(state: gameInfo)
        }
    }
    
	public func setConfiguration() {
        gameStateRepository.getGameState()
            .receive(on: DispatchQueue.main)
            .compactMap { $0 }
            .sink { [weak self] gameState in
                self?.gameInfo = gameState
            }
            .store(in: &subscriptions)
    }
    
  ...
}

setConfiguration 메서드에서 게임 상태를 구독하고, 바뀐 게임 상태를 변경합니다.

그리고 gameInfo 프로퍼티는 값이 변하면 updateViewControllers 메서드를 호출합니다.

 private func updateViewControllers(state: GameState) {
        let viewType = state.resolveViewType()
        switch viewType {
        case .submitMusic:
            navigateToSelectMusic()
        case .humming:
            navigateToHumming()
        case .rehumming:
            navigateToRehumming()
        case .submitAnswer:
            navigateToSubmitAnswer()
        case .result:
            navigateToResult()
        case .lobby:
            navigateToLobby()
        default:
            break
        }
    }

호출된 메서드는 GameState 타입 내부의 값에 따라 화면 전환을 담당하는 각각의 메서드를 호출합니다.

 private func navigateToHumming() {
        let gameStatusRepository = DIContainer.shared.resolve(GameStatusRepositoryProtocol.self)
        let playersRepository = DIContainer.shared.resolve(PlayersRepositoryProtocol.self)
        let answersRepository = DIContainer.shared.resolve(AnswersRepositoryProtocol.self)
        let recordsRepository = DIContainer.shared.resolve(RecordsRepositoryProtocol.self)
        
        let vm = HummingViewModel(
            gameStatusRepository: gameStatusRepository,
            playersRepository: playersRepository,
            answersRepository: answersRepository,
            recordsRepository: recordsRepository
        )
        let vc = HummingViewController(viewModel: vm)
        
        let guideVC = GuideViewController(type: .humming) { [weak self] in
            guard let self else { return }
            navigationController.pushViewController(vc, animated: true)
            setupNavigationBar(for: vc)
        }
        navigationController.pushViewController(guideVC, animated: true)
    }

예를 들어 navigateToHumming 메서드 내부에는 HummingViewController 객체를 생성하기 위한 모든 인스턴스를 생성하고, navigationController에 push하여 화면 전환을 진행합니다.

이런 식으로 한 곳에서만 게임 상태를 구독하고 게임 상태에 따른 화면 전환을 모두 일임함으로써 각각의 ViewController에서의 책임을 줄어들게 만들었습니다.

iOS07 프로젝트 일지

📚 문서

🫶🏻 팀 기록

🎤 프로젝트

💡 핵심 경험

🚨 트러블 슈팅

📔 학습 정리

🪄 QA

Clone this wiki locally