[RxSwift] AirPortClone: Modal Presentation

Junyoung Park·2022년 12월 28일
0

RxSwift

목록 보기
23/25
post-thumbnail
post-custom-banner

#11 Present and Dismiss a ViewController in Coordinator - RxSwift MVVM Coordinator iOS App

AirPortClone: Modal Presentation

구현 목표

  • coordinator 패턴에서의 모달 전환 구현

구현 태스크

  • Routing 프로토콜 내 모달 프레젠트 및 디스미스 함수 구현
  • 클로저를 통한 디스미스 시 메모리 누수 방지

핵심 코드

protocol RouterProtocol {
    func push(_ drawable: Drawable, isAnimated: Bool, onNavigationBack: NavigationBackClosure?)
    func pop(_ isAnimated: Bool)
    func popToRoot(_ isAnimated: Bool)
    func present(_ drawable: Drawable, isAnimated: Bool, onDismiss: NavigationBackClosure?)
}
  • 라우팅을 담당하는 프로토콜 내 present 함수 추가
func present(_ drawable: Drawable, isAnimated: Bool, onDismiss closure: NavigationBackClosure?) {
        guard let viewController = drawable.viewController else { return }
        if let closure = closure {
            closures.updateValue(closure, forKey: viewController.description)
        }
        navigationController.present(viewController, animated: isAnimated)
        viewController.presentationController?.delegate = self
    }
  • 해당 프로토콜을 따르는 라우터 클래스에서 해당 함수는 즉 모달로 프레젠트된 뷰 컨트롤러와 함께 파라미터로 건네받은 디스미스 클로저를 딕셔너리에 기록해두는 것, 즉 이전의 네비게이션 이동과 동일한 로직
extension Router: UIAdaptivePresentationControllerDelegate {
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        executeClosures(presentationController.presentedViewController)
    }
}
  • 프레젠테이션 컨트롤러 델리게이트를 따름으로써 presentationControllerDidDismiss 함수를 사용 가능
  • 모달 뷰가 사라졌을 때 executeClosures()를 사용함으로써 딕셔너리에 기록해놓은 클로저가 해당 라우터 클래스에서 실행됨
private func showAirportDetails(model: AirportModel) {
        let detailCoordinator = AirportDetailsCoordinator(router: router)
        add(coordinator: detailCoordinator)
        
        detailCoordinator.isCompleted = { [weak self, weak detailCoordinator] in
            guard let detailCoordinator = detailCoordinator else { return }
            self?.remove(coordinator: detailCoordinator)
        }
        detailCoordinator.start()
    }
  • 모달 전환이 이루어지는 곳은 테이블 뷰의 각 셀을 구현하는 AirportsCoordinator에서 클릭 이벤트가 발생하는 곳
  • detailCoordinator라는 새로운 전환이 일어날 때 isCompleted - 즉 onDismiss 클로저에 파라미터로 들어갈 클로저를 선언
  • coordinator에 들어간 메모리를 다시 제거해주는 장소
override func start() {
        let vc = AirportDetailsViewController()
        router.present(vc, isAnimated: true, onDismiss: isCompleted)
    }
  • AirportsCoordinator에서 작동하는 start() 함수
  • 실제 라우터 클래스의 present 함수가 실행되는 코드
private func setViewModel() {
        viewModel = viewModelBuilder((
            selectAirport: tableView.rx.modelSelected(AirportViewPresentable.self).asDriver(onErrorDriveWith: .empty()), ()
        ))
    }
  • AirportsViewController에서 해당 setViewModel 함수가 실행되는 구문
  • tableView.rx.modelSelected를 통해 반응형으로 현재 유저가 선택한 셀의 모델(AirportViewPresentable)을 뷰 모델 빌더에게 넘겨줄 수 있음
typealias Input = (
        selectAirport: Driver<AirportViewPresentable>, ()
    )
  • 뷰 모델이 따르는 프레젠터블 프로토콜의 인풋
private typealias RoutingAction = (airportSelectRelay: PublishRelay<AirportModel>, ())
    private let routingAction = (airportSelectRelay: PublishRelay<AirportModel>(), ())
    typealias Routing = (airportSelect: Driver<AirportModel>, ())
    lazy var router: Routing = (airportSelect: routingAction.airportSelectRelay.asDriver(onErrorDriveWith: .empty()), ())
  • 뷰 모델의 라우팅 액션 / 라우팅은 각각 이동할 뷰 컨트롤러가 가질 뷰 모델을 구성하는 역할
  • private으로 감싸 놓은 Relay를 통해 Driver를 만들어주는 역할
func process(dependencies: Dependencies) {
        input
            .selectAirport
            .map { [models = dependencies.models] viewModel in
                models.filter({ $0.code == viewModel.code}).first
            }
            .compactMap({ $0 })
            .map({ [routingAction] in
                routingAction.airportSelectRelay.accept($0)
            })
            .drive()
            .disposed(by: disposeBag)
    }
  • 입력된 Set<AirportModel> 가운데 입력된 모델과 코드가 일치하는 모델만 필터링, 라우팅 액션 Relay에 값을 들여보내는 곳

구현 화면

profile
JUST DO IT
post-custom-banner

0개의 댓글