Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
VC & VM from FuncConf Talk "From Sketch to Xcode: Building Functional Reactive View Models from the Ground Up"
//
// 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