[RxSwift] AirPortClone: MapKit

Junyoung Park·2022년 12월 29일
0

RxSwift

목록 보기
24/25
post-thumbnail

#12 Adding Pins to Map using MapKit - RxSwift MVVM Coordinator iOS App

AirPortClone: MapKit

구현 목표

구현 태스크

  • 맵뷰 구현
  • 맵뷰 어노테이션 뷰 구현
  • 현재 위치 - 해당 위치 경로 구현

핵심 코드

protocol AirportDetailsViewPresentable {
    typealias Input = ()
    typealias Output = (
        airportDetails: Driver<AirportViewPresentable>,
        mapDetails: Driver<AirportMapViewPresentable>
    )
    typealias Dependencies = (
        model: AirportModel,
        currentLocation: Observable<(lat: Double, lon: Double)?>
    )
    
    var input: AirportDetailsViewPresentable.Input { get }
    var output: AirportDetailsViewPresentable.Output { get }
    
    typealias ViewModelBuilder = (AirportDetailsViewPresentable.Input) -> (AirportDetailsViewPresentable)
}
  • 맵뷰와 함께 해당 공항의 정보를 표현할 뷰에 해당할 뷰 모델 프로토콜
  • 인풋을 통해 뷰 모델 자체를 리턴하는 뷰 모델 빌더 및 아웃풋을 구성하기 위한 디펜던시
    private static func transform(input: AirportDetailsViewPresentable.Input, dependencies: AirportDetailsViewPresentable.Dependencies) -> AirportDetailsViewPresentable.Output {
        let airportDetails: Driver<AirportViewPresentable> = dependencies
            .currentLocation
            .compactMap({ $0 })
            .map { [airportModel = dependencies.model] currentLocation in
                AirportViewModel(model: airportModel, currentLocation: currentLocation)
            }
            .asDriver(onErrorDriveWith: .empty())
        let mapDetails: Driver<AirportMapViewPresentable> = dependencies
            .currentLocation
            .compactMap({ $0 })
            .map { [airportModel = dependencies.model] currentLocation in
                guard
                    let lat = Double(airportModel.lat),
                    let lon = Double(airportModel.lon) else { throw URLError(.badURL) }
                
                return AirportMapViewModel(airportLocation: (lat: lat, lon: lon), airport: (name: airportModel.name, city: airportModel.city), currentLocation: currentLocation)
            }
            .asDriver(onErrorDriveWith: .empty())
        return (
            airportDetails: airportDetails,
            mapDetails: mapDetails
        )
    }
  • 건네받은 디펜던시의 값을 통해 공항 정보 및 맵 뷰 정보를 업데이트 → Driver로 캐스팅해서 UI 업데이트에 적용
import Foundation

protocol AirportMapViewPresentable {
    var airport: (name: String, city: String) { get }
    var currentLocation: (lat: Double, lon: Double) { get }
    var airportLocation: (lat: Double, lon: Double) { get }
    
}
  • 맵뷰를 그릴 때 사용할 별도의 프로토콜
private var viewModel: AirportDetailsViewPresentable?
    var viewModelBuilder: AirportDetailsViewPresentable.ViewModelBuilder!
  • 뷰 컨트롤러에서 private으로 숨긴 뷰 모델과 달리 외부 접근 가능한 뷰 모델 빌더
override func start() {
        let vc = AirportDetailsViewController()
        let locationService = LocationService.shared
        vc.viewModelBuilder = {
            AirportDetailsViewModel(input: $0, dependencies: (model: self.model, currentLocation: locationService.currentLocation))
        }
        router.present(vc, isAnimated: true, onDismiss: isCompleted)
    }
  • 디테일 뷰 컨트롤러를 coordinator에 띄우는 단계인 start() 함수에서 외부 접근 가능한 뷰 모델 빌더의 클로저를 현 시점에서 선언
  • 뷰 모델을 만들 때 필요한 인풋 및 디펜던시가 존재하는 공간이 현재 coordinator이기 때문
    private func bind() {
        viewModel?.output
            .airportDetails
            .map({ [weak self] viewModel in
                self?.nameLabel.text = viewModel.name
                self?.distanceLabel.text = viewModel.formattedDistance
                self?.countryLabel.text = viewModel.address
                self?.lengthLabel.text = viewModel.runwayLength
            })
            .drive()
            .disposed(by: disposeBag)
        viewModel?.output
            .mapDetails
            .map({ [weak self] viewModel in
                let currentPoint = CLLocationCoordinate2D(latitude: viewModel.currentLocation.lat, longitude: viewModel.currentLocation.lon)
                let airportPoint = CLLocationCoordinate2D(latitude: viewModel.airportLocation.lat, longitude: viewModel.airportLocation.lon)
                let currentPin = AirportPin(name: "Current", coordinate: currentPoint)
                let airportPin = AirportPin(name: viewModel.airport.name, city: viewModel.airport.city, coordinate: airportPoint)
                self?.mapView.addAnnotations([currentPin, airportPin])
                print("MapView add annotations")
                let currentPlacemark = MKPlacemark(coordinate: currentPoint)
                let destinationPlacemark = MKPlacemark(coordinate: airportPoint)
                let directionRequest = MKDirections.Request()
                directionRequest.source = MKMapItem(placemark: currentPlacemark)
                directionRequest.destination = MKMapItem(placemark: destinationPlacemark)
                directionRequest.transportType = .automobile
                let directions = MKDirections(request: directionRequest)
                directions.calculate { response, error in
                    guard
                        let route = response?.routes.first,
                        error == nil else { return }
                    self?.mapView.addOverlay(route.polyline
                                             , level: .aboveRoads)
                    UIView.animate(withDuration: 1) {
                        let mapRect = route.polyline.boundingMapRect
                        let region = MKCoordinateRegion(mapRect)
                        self?.mapView.setRegion(region, animated: true)
                    }
                }
            })
            .drive()
            .disposed(by: disposeBag)
    }
  • Driver에 의해 관찰 가능한 데이터가 들어올 때 바인딩되는 곳
  • 스택 뷰에 해당 맵뷰의 정보를 라벨로 들여보내고, 커스텀 맵 어노테이션을 통해 해당 위치를 맵뷰에 표시, 현재 위치와의 경로 등을 표현

구현 화면

profile
JUST DO IT

0개의 댓글