Skip to content

Instantly share code, notes, and snippets.

@fnazarios
Last active July 5, 2018 19:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fnazarios/80e2c8cc1bad77eb404ae4a0d95b7cdd to your computer and use it in GitHub Desktop.
Save fnazarios/80e2c8cc1bad77eb404ae4a0d95b7cdd to your computer and use it in GitHub Desktop.
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
}
}
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
}
}
}
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
}
}
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()
}
}
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)
}
}
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