-
Notifications
You must be signed in to change notification settings - Fork 1
화면 전환(Game NavigationController)
저희는 세션을 통한 연결이 아니기 때문에 클라이언트 측에서 DB의 현재 속해 있는 방의 게임 상태 값을 보고 화면을 보여주어야 합니다.
그렇다면 화면을 이동하기 위해서는 각 ViewController가 DB의 게임 상태 값을 구독하고,
값에 따라 화면을 이동 시켜주어야 하는 상태가 됩니다.
이렇게 되면 각 VC들은 DB의 게임 상태를 bind하는 함수가 추가되어야 하고,
화면을 이동 시키기 위해 다음 화면에 필요한 인스턴스를 생성하고, 주입하고 끝으로 화면을 이동 시켜주어야 하는 책임이 발생하게 됩니다.
해당 방식은 꽤나 번거롭고 화면마다 다음 화면으로 넘어갈 작업을 작성해주어야 해 까다롭기까지 한 것 같습니다.
화면 전환에 있어 가장 유명한 패턴인 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의 스택에 넣어줌으로써 화면 전환을 진행합니다.
저희 프로젝트는 게임이기 때문에 모든 플레이어가 같은 화면을 볼 필요가 있습니다.
그러므로 각 VC는 DB의 게임 상태 값을 구독하고 있고 게임 상태 값에 따라 Coordinator에게 화면 전환을 요청해야 합니다.
이렇게 되면 각 VC들은 게임 상태 값을 계속 구독해주고 있어야 하는 책임이 생깁니다. 저희는 이걸 막고 싶었습니다.
일단은 게임 상태를 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에서의 책임을 줄어들게 만들었습니다.
- 📒 기획의 과정과 의도
- 📒 swift6 도입기 ‐ @unchecked Sendable을 사용해야만 했던 이유
- 📒 WaveForm(파형) 제작기
- 📒 프로젝트 구조와 이유
- 📒 화면 전환(Game NavigationController)
- 📒 DIContainer를 사용한 계기
- 📒 AudioHelper 제작기
- 📒 음악 플레이어의 compact 버전 제작기
- 📒 Combine을 이용한 데이터 전달
- 📒 파이어베이스를 쓰며 있었던 일
- 📒 캐싱 모듈 구현과 문제점
- 📒 로그 시스템 제작기
- ❗ Data 끼리의 비교
- ❗ 프레임워크 Reference 안잡히는 문제
- ❗ actor 안에서 timer가 실행되지 않는 문제
- ❗ NSLayoutConstraint 옵셔널 문제
- ❗ 테이블 뷰가 보고 있는 배열과 bind하고 있는 배열 간의 race condition 문제
- ❗ 테스트끼리의 독립성
- ❗ 네트워크 테스팅 시 Error 핸들링
- ❗ 여러 클라이언트가 서버에 동시 요청시, 데이터가 반영이 안되는 이슈 해결
- ❗ 의존성 framework 추가시 불러오지 못하는 문제
- ❗ Timer를 6초 설정해도 더 실행되는 문제
- ❗ Music Kit Data Request 에러
- ❗ DI Container 에서 생성한 인스턴스가 동시에 존재 하는 이슈