Created
December 21, 2018 18:52
-
-
Save dannyhertz/9eb4247e784e0c3b2ff8ec60098630a2 to your computer and use it in GitHub Desktop.
VC & VM from FuncConf Talk "From Sketch to Xcode: Building Functional Reactive View Models from the Ground Up"
This file contains hidden or 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
// | |
// TalkDockViewController.swift | |
// Dockee | |
// | |
// Created by Danny Hertz on 11/30/18. | |
// Copyright © 2018 Danny Hertz. All rights reserved. | |
// | |
import Foundation | |
import RxCocoa | |
import RxSwift | |
import SnapKit | |
import MapKit | |
import Overture | |
private let updatedDateFormatter = with( | |
DateFormatter(), | |
concat( | |
mut(\DateFormatter.dateFormat, "h:mm:ss a"), | |
mut(\DateFormatter.locale, Current.locale) | |
) | |
) | |
extension CLLocation { | |
static let nyc = CLLocation(latitude: 40.7128, longitude: -74.0060) | |
} | |
final class TalkDockViewController: UIViewController, MKMapViewDelegate { | |
private let disposeBag = DisposeBag() | |
private lazy var mapView: MKMapView = { | |
let view = MKMapView() | |
view.showsUserLocation = true | |
view.delegate = self | |
return view | |
}() | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
title = "Dock Finder" | |
view.backgroundColor = .white | |
let titleLabel = with( | |
UILabel(), | |
concat( | |
mut(\.textColor, UIColor.darkGray), | |
mut(\.font, UIFont.boldSystemFont(ofSize: 22)) | |
) | |
) | |
let subtitleLabel = with( | |
UILabel(), | |
concat( | |
mut(\.textColor, UIColor.gray), | |
mut(\.font, UIFont.systemFont(ofSize: 18)) | |
) | |
) | |
let dockCountLabel = with( | |
UILabel(), | |
concat( | |
mut(\.textColor, UIColor.white), | |
mut(\.font, UIFont.boldSystemFont(ofSize: 32)) | |
) | |
) | |
let dockCountContainerView = with( | |
UIView(), | |
mut(\.layer.cornerRadius, 8) | |
) | |
let titleStack = with( | |
UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]), | |
concat( | |
mut(\UIStackView.axis, .vertical), | |
mut(\UIStackView.spacing, 6) | |
) | |
) | |
let dockLabel = with( | |
UILabel(), | |
concat( | |
mut(\.text, "DOCKS"), | |
mut(\.textColor, UIColor.white), | |
mut(\.font, UIFont.boldSystemFont(ofSize: 12)) | |
) | |
) | |
let dockLabelStack = with( | |
UIStackView(arrangedSubviews: [dockCountLabel, dockLabel]), | |
concat( | |
mut(\UIStackView.axis, .vertical), | |
mut(\UIStackView.spacing, 2), | |
mut(\UIStackView.alignment, .center) | |
) | |
) | |
let refreshButton = UIButton(type: .roundedRect) | |
refreshButton.setTitle("Refresh", for: .normal) | |
refreshButton.contentEdgeInsets = .init(top: 10, left: 20, bottom: 10, right: 20) | |
refreshButton.layer.borderColor = UIColor.gray.cgColor | |
refreshButton.layer.borderWidth = 1 | |
let updatedLabel = with( | |
UILabel(), | |
concat( | |
mut(\.textColor, UIColor.gray), | |
mut(\.font, UIFont.boldSystemFont(ofSize: 16)) | |
) | |
) | |
dockCountContainerView.backgroundColor = UIColor(hexString: "00b894") | |
dockCountContainerView.addSubview(dockLabelStack) | |
let contentStack = with( | |
UIStackView(arrangedSubviews: [titleStack, dockCountContainerView]), | |
concat( | |
mut(\UIStackView.axis, .horizontal), | |
mut(\UIStackView.spacing, 15), | |
mut(\UIStackView.distribution, .equalSpacing), | |
mut(\UIStackView.alignment, .center) | |
) | |
) | |
view.addSubview(contentStack) | |
view.addSubview(mapView) | |
view.addSubview(refreshButton) | |
view.addSubview(updatedLabel) | |
dockLabelStack.snp.makeConstraints { make in | |
make.edges.equalToSuperview().inset(UIEdgeInsets.init(top: 10, left: 8, bottom: 10, right: 8)) | |
} | |
contentStack.snp.makeConstraints { make in | |
make.top.equalTo(topLayoutGuide.snp.bottom).offset(80) | |
make.leading.trailing.equalToSuperview().inset(40) | |
} | |
refreshButton.snp.makeConstraints { make in | |
make.centerX.equalToSuperview() | |
make.top.equalTo(contentStack.snp.bottom).offset(40) | |
} | |
updatedLabel.snp.makeConstraints { make in | |
make.centerX.equalToSuperview() | |
make.top.equalTo(refreshButton.snp.bottom).offset(40) | |
} | |
mapView.snp.makeConstraints { make in | |
make.top.equalTo(updatedLabel.snp.bottom).offset(60) | |
make.leading.trailing.bottom.equalToSuperview() | |
} | |
let ( | |
stationNameLabelText, | |
stationDistanceLabelText, | |
dockCountLabelText, | |
mapViewCenteredLocation, | |
lastUpdatedLabelText, | |
) = talkDockViewModel( | |
reloadButtonTapped: refreshButton | |
.rx | |
.controlEvent(.touchUpInside) | |
.asObservable(), | |
viewDidLoad: .just(()) | |
) | |
stationNameLabelText | |
.bindOnMain(to: titleLabel.rx.text) | |
.disposed(by: disposeBag) | |
stationDistanceLabelText | |
.bindOnMain(to: subtitleLabel.rx.text) | |
.disposed(by: disposeBag) | |
dockCountLabelText | |
.bindOnMain(to: dockCountLabel.rx.text) | |
.disposed(by: disposeBag) | |
mapViewCenteredLocation | |
.bindOnMain { [weak self] location in | |
self?.mapView.setCenter(location.coordinate, animated: true) | |
} | |
.disposed(by: disposeBag) | |
lastUpdatedLabelText | |
.bindOnMain(to: updatedLabel.rx.text) | |
.disposed(by: disposeBag) | |
} | |
} | |
func talkDockViewModel( | |
reloadButtonTapped: Observable<Void>, | |
viewDidLoad: Observable<Void> | |
) -> ( | |
stationNameLabelText: Observable<String>, | |
stationDistanceLabelText: Observable<String>, | |
dockCountLabelText: Observable<String>, | |
mapViewCenteredLocation: Observable<CLLocation>, | |
lastUpdatedLabelText: Observable<String> | |
) { | |
let fetchData = Observable.merge( | |
viewDidLoad, | |
reloadButtonTapped | |
) | |
let currentLocation = fetchData | |
.flatMapLatest { Current.locationManager.location } | |
.share() | |
let stations = currentLocation | |
.flatMapLatest { Current.api.closestStations(to: $0) } | |
.share() | |
let closestStation = stations | |
.filterMap { $0.first } | |
let stationNameLabelText = closestStation | |
.map { $0.name } | |
.startWith("Loading...") | |
let stationDistanceLabelText = closestStation | |
.withLatestFrom(currentLocation) { $0.location.distance(from: $1) } | |
.map { "\(Int($0))m away" } | |
.startWith("Calculating...") | |
let dockCountLabelText = closestStation | |
.map { "\($0.docksAvailable)" } | |
.startWith("-") | |
let mapViewCenteredLocation = closestStation | |
.map { $0.location } | |
.startWith(CLLocation.nyc) | |
let lastUpdatedLabelText = closestStation | |
.map { _ in | |
"Last Updated: \(updatedDateFormatter.string(from: Current.date()))" | |
} | |
.startWith("Last Updated: Never") | |
return ( | |
stationNameLabelText: stationNameLabelText, | |
stationDistanceLabelText: stationDistanceLabelText, | |
dockCountLabelText: dockCountLabelText, | |
mapViewCenteredLocation: mapViewCenteredLocation, | |
lastUpdatedLabelText: lastUpdatedLabelText | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment