Last active
July 5, 2018 19:17
-
-
Save fnazarios/80e2c8cc1bad77eb404ae4a0d95b7cdd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
protocol ApiExposable { | |
var baseURL: URL { get } | |
var path: String { get } | |
var method: HTTPMethod { get } | |
var parameters: [String: Any] { get } | |
var isTokenNeeded: Bool { get } | |
} | |
class Api<E: Decodable> { | |
private var api: ApiExposable | |
private let disposeBag = DisposeBag() | |
init(api: ApiExposable) { | |
self.api = api | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public enum ApiBus { | |
case search(lineCode: String) | |
case snapToRoad(latLonPairs: String) | |
case eta(originLat: Double, originLon: Double, destinationLat: Double, destinationLon: Double) | |
} | |
extension ApiBus: ApiExposable { | |
public var baseURL: URL { | |
switch self { | |
case .search(_): | |
return URL(string: Environment.apiURL)! | |
case .snapToRoad(_): | |
return URL(string: Environment.googleRoadsApiUrl)! | |
case .eta(_, _, _, _): | |
return URL(string: Environment.googleMapApiUrl)! | |
} | |
} | |
public var path: String { | |
switch self { | |
case .search(let lineCodes): | |
return "v1/lines/\(lineCodes)" | |
case .snapToRoad(_): | |
return "snapToRoads" | |
case .eta(_, _, _, _): | |
return "directions/json" | |
} | |
} | |
public var method: HTTPMethod { | |
switch self { | |
default: | |
return .get | |
} | |
} | |
public var parameters: [String: Any] { | |
switch self { | |
case .snapToRoad(let latLonPairs): | |
return ["path": latLonPairs, "key": Environment.googleRoadsApiKey, "interpolate": true] | |
case .eta(let originLat, let originLon, let destinationLat, let destinationLon): | |
return ["origin": "\(originLat),\(originLon)", "destination": "\(destinationLat),\(destinationLon)", "key": Environment.googleRoadsApiKey] | |
default: | |
return [:] | |
} | |
} | |
public var isTokenNeeded: Bool { | |
switch self { | |
default: | |
return true | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import MapKit | |
import RxSwift | |
class MapLineViewController: UIViewController { | |
@IBOutlet weak var mapLineView: MKMapView! | |
private let viewModel: MapLineViewModelType = MapLineViewModel() | |
private let disposeBag = DisposeBag() | |
var favoriteSelectedLine: Route? | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
bindViewModel() | |
navigationController?.navigationItem.largeTitleDisplayMode = .automatic | |
if let favLine = favoriteSelectedLine { | |
viewModel.inputs.configure(with: favLine) | |
Analytics.shared.log(event: .mapView, properties: ["destination": favLine.destination]) | |
} | |
} | |
private func bindViewModel() { | |
viewModel.outputs.title | |
.bind(to: self.rx.title) | |
.disposed(by: disposeBag) | |
viewModel.outputs.stops | |
.subscribe(onNext: { (stops) in | |
self.traceRoute(stops) | |
}) | |
.disposed(by: disposeBag) | |
viewModel.outputs.nearestStop | |
.subscribe(onNext: { (stop) in | |
self.mapLineView.addAnnotation(stop) | |
}) | |
.disposed(by: disposeBag) | |
viewModel.outputs.vehicles | |
.observeOn(MainScheduler.instance) | |
.subscribeOn(MainScheduler.instance) | |
.subscribe(onNext: { (vehicles) in | |
let vehiclesAnnotations = self.mapLineView.annotations.filter({ $0 is VehicleAnnotation }) | |
self.mapLineView.removeAnnotations(vehiclesAnnotations) | |
self.mapLineView.addAnnotations(vehicles) | |
}) | |
.disposed(by: disposeBag) | |
} | |
private lazy var directionsRequest: MKDirectionsRequest = { | |
let directionsRequest = MKDirectionsRequest() | |
directionsRequest.transportType = .automobile | |
return directionsRequest | |
}() | |
private func route(from: MKMapItem?, to: MKMapItem?, completion: @escaping (() -> Void)) { | |
directionsRequest.source = from | |
directionsRequest.destination = to | |
MKDirections(request: directionsRequest).calculate { [weak self] (response, _) in | |
if let route = response?.routes[0] { | |
dump("Received route \(String(describing: response?.routes[0]))") | |
self?.mapLineView.add(route.polyline, level: MKOverlayLevel.aboveRoads) | |
} | |
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { | |
completion() | |
} | |
} | |
} | |
private var currentPlacemarkIndex: Int = 0 | |
private var allMapItems: [MKMapItem] = [] | |
private func continueRouting() { | |
guard currentPlacemarkIndex < (allMapItems.count - 1) else { return } | |
let from = allMapItems[currentPlacemarkIndex] | |
let to = allMapItems[currentPlacemarkIndex + 1] | |
route(from: from, to: to) { [weak self] in | |
self?.currentPlacemarkIndex += 1 | |
self?.continueRouting() | |
} | |
} | |
private func traceRoute(_ stops: [StopAnnotation]) { | |
let coordinates = stops.map { $0.coordinate } | |
let polyline = MKGeodesicPolyline(coordinates: coordinates, count: coordinates.count) | |
mapLineView.add(polyline) | |
Analytics.shared.log(event: .loadRoute) | |
guard CLLocationCoordinate2DIsValid(LocationWorker.lastUserCoordinate) else { return } | |
let coordinateRegion = MKCoordinateRegion(center: LocationWorker.lastUserCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.10, longitudeDelta: 0.10)) | |
mapLineView.setRegion(coordinateRegion, animated: true) | |
} | |
} | |
extension MapLineViewController: MKMapViewDelegate { | |
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { | |
guard type(of: annotation) != MKUserLocation.self else { return nil } | |
if annotation is StopAnnotation { | |
let identifier = "StopAnnotationIdentifier" | |
var view: StopAnnotationView | |
if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? StopAnnotationView { | |
view = dequeuedView | |
} else { | |
view = StopAnnotationView(annotation: annotation, reuseIdentifier: identifier) | |
} | |
view.canShowCallout = true | |
view.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0) | |
return view | |
} else if annotation is VehicleAnnotation { | |
let identifier = "VehicleAnnotationIdentifier" | |
var view: VehicleAnnotationView | |
if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? VehicleAnnotationView { | |
view = dequeuedView | |
} else { | |
view = VehicleAnnotationView(annotation: annotation, reuseIdentifier: identifier) | |
} | |
view.image = (annotation as? VehicleAnnotation)?.pinImage | |
view.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0) | |
return view | |
} | |
return nil | |
} | |
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { | |
guard let polyline = overlay as? MKPolyline else { | |
return MKOverlayRenderer() | |
} | |
let renderer = MKPolylineRenderer(polyline: polyline) | |
renderer.lineWidth = 3.0 | |
renderer.alpha = 0.7 | |
renderer.strokeColor = #colorLiteral(red: 0.7529411765, green: 0.5058823529, blue: 1, alpha: 1) | |
return renderer | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RxSwift | |
import RxCocoa | |
import MapKit | |
import CoreLocation | |
protocol MapLineViewModelInputs { | |
func configure(with route: Route) | |
} | |
protocol MapLineViewModelOutputs { | |
var stops: Observable<[StopAnnotation]> { get } | |
var vehicles: Observable<[VehicleAnnotation]> { get } | |
var nearestStop: Observable<StopAnnotation> { get } | |
var title: Observable<String> { get } | |
} | |
protocol MapLineViewModelType { | |
var inputs: MapLineViewModelInputs { get } | |
var outputs: MapLineViewModelOutputs { get } | |
} | |
final class MapLineViewModel: MapLineViewModelType, MapLineViewModelInputs, MapLineViewModelOutputs { | |
internal var inputs: MapLineViewModelInputs { return self } | |
internal var outputs: MapLineViewModelOutputs { return self } | |
var stops: Observable<[StopAnnotation]> | |
var vehicles: Observable<[VehicleAnnotation]> | |
var nearestStop: Observable<StopAnnotation> | |
var title: Observable<String> | |
private static var route: Route? | |
private let disposeBag = DisposeBag() | |
init() { | |
_ = LocationWorker.shared | |
title = configureWithRouteProperty | |
.map { "\($0.codeLine) \($0.destination)" } | |
let newVehiclesOnRoute = reloadRouteProperty | |
.map { $0.codeLine } | |
.flatMap { | |
MapLineViewModel.search($0) | |
} | |
vehicles = Observable.merge([configureWithRouteProperty, newVehiclesOnRoute]) | |
.map { $0.vehicles } | |
.ignoreNil() | |
.map({ (v) -> [VehicleAnnotation] in | |
return v.map({ (item) -> VehicleAnnotation in | |
let img = item.way == Way.forward ? UIImage(named: "bus_pin_back") : UIImage(named: "bus_pin_foward") | |
return VehicleAnnotation(coordinate: item.coordinate, pinImage: img) | |
}) | |
}) | |
let allStops = configureWithRouteProperty | |
.map { $0.pods } | |
stops = allStops | |
.map { $0.map({ "\($0.latitude),\($0.longitude)" }) } | |
.map { $0.joined(separator: "|") } | |
.flatMap({ (locations) -> Observable<[SnappedPoint]> in | |
return Api<SnapResult>(api: ApiBus.snapToRoad(latLonPairs: locations)) | |
.fireInTheHole() | |
.map { $0.snappedPoints } | |
.ignoreNil() | |
}) | |
.map { $0.map({ StopAnnotation(address: "", coordinate: $0.coordinate) }) } | |
nearestStop = allStops | |
.map { (stops) -> [PointOfDeparture] in | |
let userGeoHash = try? GeohashBits(location: LocationWorker.lastUserCoordinate.location, characterPrecision: 9) | |
let userGeoHashBits = userGeoHash?.bits ?? 0 | |
let sorted = stops.sorted(by: { (lhs, rhs) -> Bool in | |
guard let lhsHash = lhs.geoHash, let rhsHash = rhs.geoHash else { return false } | |
return (Int(lhsHash.bits) - Int(userGeoHashBits)).magnitude < (Int(rhsHash.bits) - Int(userGeoHashBits)).magnitude | |
}) | |
return sorted | |
} | |
.map { $0.map({ StopAnnotation(address: $0.address, coordinate: $0.coordinate) }) } | |
.map { $0.first } | |
.ignoreNil() | |
configureWithRouteProperty | |
.bind(onNext: { MapLineViewModel.route = $0 }) | |
.disposed(by: disposeBag) | |
Observable<Int>.interval(15, scheduler: MainScheduler.asyncInstance) | |
.bind { (_) in | |
self.reload() | |
} | |
.disposed(by: disposeBag) | |
} | |
private let configureWithRouteProperty = PublishSubject<Route>() | |
func configure(with route: Route) { | |
configureWithRouteProperty.onNext(route) | |
} | |
private let reloadRouteProperty = PublishSubject<Route>() | |
private func reload() { | |
guard let r = MapLineViewModel.route else { return } | |
reloadRouteProperty.onNext(r) | |
} | |
private static func search(_ shortName: String) -> Observable<Route> { | |
return Api<LineResult>(api: ApiBus.search(lineCode: shortName)) | |
.fireInTheHole() | |
.map { $0.result } | |
.ignoreNil() | |
.map({ (routes) -> [Route] in | |
return routes.filter({ (r) -> Bool in | |
let way = MapLineViewModel.route?.way ?? Way.forward | |
return r.way == way | |
}) | |
}) | |
.map { $0.first } | |
.ignoreNil() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import RxSwift | |
import RxCocoa | |
class WorkoutIntervalCell: UITableViewCell { | |
private let viewModel: WorkoutIntervalCellViewModelType = WorkoutIntervalCellViewModel() | |
private let disposeBag = DisposeBag() | |
@IBOutlet weak var intervalLabel: UILabel! | |
@IBOutlet weak var distanceLabel: UILabel! | |
@IBOutlet weak var durationLabel: UILabel! | |
@IBOutlet weak var averagePaceLabel: UILabel! | |
override func awakeFromNib() { | |
super.awakeFromNib() | |
bindViewModel() | |
} | |
private func bindViewModel() { | |
viewModel.outputs.interval | |
.bind(to: intervalLabel.rx.text) | |
.disposed(by: disposeBag) | |
viewModel.outputs.distance | |
.bind(to: distanceLabel.rx.text) | |
.disposed(by: disposeBag) | |
viewModel.outputs.duration | |
.bind(to: durationLabel.rx.text) | |
.disposed(by: disposeBag) | |
viewModel.outputs.averagePace | |
.bind(to: averagePaceLabel.rx.text) | |
.disposed(by: disposeBag) | |
} | |
func configure(withSplit split: Split) { | |
viewModel.inputs.configure(withSplit: split) | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RxSwift | |
import RxCocoa | |
import Action | |
import HealthKit | |
protocol WorkoutIntervalCellViewModelInputs { | |
func configure(withSplit: Split) | |
} | |
protocol WorkoutIntervalCellViewModelOutputs { | |
var interval: Observable<String> { get } | |
var distance: Observable<String> { get } | |
var duration: Observable<String> { get } | |
var averagePace: Observable<String> { get } | |
} | |
protocol WorkoutIntervalCellViewModelType { | |
var inputs: WorkoutIntervalCellViewModelInputs { get } | |
var outputs: WorkoutIntervalCellViewModelOutputs { get } | |
} | |
final class WorkoutIntervalCellViewModel: WorkoutIntervalCellViewModelType, WorkoutIntervalCellViewModelInputs, WorkoutIntervalCellViewModelOutputs { | |
internal var inputs: WorkoutIntervalCellViewModelInputs { return self } | |
internal var outputs: WorkoutIntervalCellViewModelOutputs { return self } | |
internal var interval: Observable<String> | |
internal var distance: Observable<String> | |
internal var duration: Observable<String> | |
internal var averagePace: Observable<String> | |
private let disposeBag = DisposeBag() | |
init() { | |
interval = configureWithSplitProperty | |
.map { $0.name } | |
distance = configureWithSplitProperty | |
.map { $0.distance } | |
.map { HKQuantity(unit: distanceUnit, doubleValue: $0) } | |
.map { format(distance: $0) } | |
duration = configureWithSplitProperty | |
.map { $0.duration } | |
.map { format(duration: $0) } | |
averagePace = configureWithSplitProperty | |
.map({ (split) -> TimeInterval in | |
let distance = HKQuantity(unit: distanceUnit, doubleValue: split.distance).doubleValue(for: paceUnit) | |
guard distance > 0 && split.duration > 0 else { return 0 } | |
return split.duration / distance | |
}) | |
.map { $0 == 0 ? "--" : format(pace: $0) } | |
} | |
private let configureWithSplitProperty = PublishSubject<Split>() | |
func configure(withSplit split: Split) { | |
configureWithSplitProperty.onNext(split) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment