Skip to content

Commit

Permalink
Rewrite POI data source, clean up Timetable and view models
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit 5006ea62e508d0c2822c3ae0be5db1d5ef1157be
Author: Elliott Williams <[email protected]>
Date:   Mon Oct 2 18:29:16 2017 -0400

    Rename POITableDataSource2 -> POITableDataSource

commit 8b729ff2d91ac99a05f8ceb17834512c0c79ed2a
Author: Elliott Williams <[email protected]>
Date:   Mon Oct 2 18:25:30 2017 -0400

    Keep track of section badges and re-assign them as ordering changes

commit 8c18438481a700fada55ebc8052615f7c1b6a986
Author: Elliott Williams <[email protected]>
Date:   Mon Oct 2 12:43:00 2017 -0400

    Show station map annotations correctly.

commit 80aba9702c70ca4807662c21687c717116eaf1ee
Author: Elliott Williams <[email protected]>
Date:   Mon Oct 2 12:10:14 2017 -0400

    Rewrite of POITableDataSource to use Dwifft, reduce Timetable magic

    Logic is moved out of higher level view controllers and view models. Timetable
    no longer has `more` continuations that try to stream upcoming arrivals.
    Instead, the data source talks to Timetable directly to populate itself. The
    POI table informs the data source of nearby stations (sections), and tell the
    data source when a section is visible, triggering the Timetable call. When an
    arrival departs, the data source reissues the Timetable call to obtain new
    arrivals.
  • Loading branch information
elliottwilliams committed Oct 2, 2017
1 parent 113da93 commit 70a918e
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 442 deletions.
12 changes: 6 additions & 6 deletions Proper.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
26517F541F803FE7001710FD /* OverlayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26517F531F803FE7001710FD /* OverlayButton.swift */; };
26517F551F804643001710FD /* Argo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26E0384F1D8DE9ED0006B265 /* Argo.framework */; };
26517F581F804659001710FD /* Runes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C7FAB41ECC03B8006B7101 /* Runes.framework */; };
26517F5D1F81DACF001710FD /* POITableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26517F5C1F81DACF001710FD /* POITableDataSource.swift */; };
26517F5E1F8284FC001710FD /* ArrivalsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2696D7F81E7DF78300ABD701 /* ArrivalsViewModel.swift */; };
266111D21D51521500D8F46B /* UIColorDecodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 266111D11D51521500D8F46B /* UIColorDecodeTests.swift */; };
266111DD1D5378B500D8F46B /* NilConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 266111DC1D5378B500D8F46B /* NilConnection.swift */; };
266111DE1D5380D300D8F46B /* ConnectionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2612F9FE1D2CCAE5005103E3 /* ConnectionMock.swift */; };
Expand Down Expand Up @@ -58,7 +60,6 @@
26C7FB201ED16D6F006B7101 /* ArrivalsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A8D6011D484944000434F9 /* ArrivalsTableViewController.swift */; };
26C7FB211ED16D6F006B7101 /* RouteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AE681B1D601D0B0031E534 /* RouteViewController.swift */; };
26C7FB221ED16D6F006B7101 /* StationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AE681D1D612B810031E534 /* StationTableViewController.swift */; };
26C7FB231ED16D6F006B7101 /* POITableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2696D8021E7ED45A00ABD701 /* POITableDataSource.swift */; };
26C7FB241ED16D6F006B7101 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A424B31BD2C1320035754F /* AppDelegate.swift */; };
26C7FB251ED16D6F006B7101 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CDF9581D7B344200E53F82 /* Box.swift */; };
26C7FB261ED16D6F006B7101 /* EventLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DBCF481D3FF59A00B22562 /* EventLog.swift */; };
Expand Down Expand Up @@ -86,7 +87,6 @@
26C7FB3D1ED16D6F006B7101 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 264B2D2C1E228781007EE6D5 /* Date.swift */; };
26C7FB3E1ED16D6F006B7101 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B526D11E1C0AA700092705 /* Location.swift */; };
26C7FB3F1ED16D6F006B7101 /* Timetable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 264B2D2A1E1EF5E4007EE6D5 /* Timetable.swift */; };
26C7FB401ED16D6F006B7101 /* ArrivalsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2696D7F81E7DF78300ABD701 /* ArrivalsViewModel.swift */; };
26C7FB411ED16D6F006B7101 /* SignalChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2696D7FC1E7DFAC200ABD701 /* SignalChain.swift */; };
26C7FB421ED16D6F006B7101 /* NearbyStationsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2696D7FE1E7DFB7E00ABD701 /* NearbyStationsViewModel.swift */; };
26C7FB431ED16D6F006B7101 /* POIViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A0FEFF1E149AF200FF265A /* POIViewModel.swift */; };
Expand Down Expand Up @@ -184,6 +184,7 @@
2650ED9F1C41CFCC006CCD42 /* Array.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
26517F531F803FE7001710FD /* OverlayButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayButton.swift; sourceTree = "<group>"; };
26517F561F804645001710FD /* Argo.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = Argo.framework.dSYM; path = Carthage/Build/iOS/Argo.framework.dSYM; sourceTree = "<group>"; };
26517F5C1F81DACF001710FD /* POITableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POITableDataSource.swift; sourceTree = "<group>"; };
266111D11D51521500D8F46B /* UIColorDecodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColorDecodeTests.swift; sourceTree = "<group>"; };
266111D71D52EA6100D8F46B /* MutableModelTestSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutableModelTestSpec.swift; sourceTree = "<group>"; };
266111DC1D5378B500D8F46B /* NilConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NilConnection.swift; sourceTree = "<group>"; };
Expand All @@ -202,7 +203,6 @@
2696D7FC1E7DFAC200ABD701 /* SignalChain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalChain.swift; sourceTree = "<group>"; };
2696D7FE1E7DFB7E00ABD701 /* NearbyStationsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NearbyStationsViewModel.swift; sourceTree = "<group>"; };
2696D8001E7E3B1D00ABD701 /* Badge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = "<group>"; };
2696D8021E7ED45A00ABD701 /* POITableDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = POITableDataSource.swift; sourceTree = "<group>"; };
2696D8051E7ED64D00ABD701 /* MKCircle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MKCircle.swift; sourceTree = "<group>"; };
2698CF001D8B8FC400E6174A /* RoutesCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = RoutesCollectionViewCell.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
26A0FEFF1E149AF200FF265A /* POIViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = POIViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -449,7 +449,7 @@
2696D8041E7ED45F00ABD701 /* Data Sources */ = {
isa = PBXGroup;
children = (
2696D8021E7ED45A00ABD701 /* POITableDataSource.swift */,
26517F5C1F81DACF001710FD /* POITableDataSource.swift */,
);
path = "Data Sources";
sourceTree = "<group>";
Expand Down Expand Up @@ -828,6 +828,7 @@
26C7FB431ED16D6F006B7101 /* POIViewModel.swift in Sources */,
26C7FB471ED16D6F006B7101 /* POIStationAnnotationView.swift in Sources */,
26C7FB3C1ED16D6F006B7101 /* RouteStop.swift in Sources */,
26517F5E1F8284FC001710FD /* ArrivalsViewModel.swift in Sources */,
26C7FB331ED16D6F006B7101 /* MutableRoute.swift in Sources */,
26C7FB1A1ED16D6F006B7101 /* LastEventCache.swift in Sources */,
26C7FB1D1ED16D6F006B7101 /* POITableViewController.swift in Sources */,
Expand All @@ -845,7 +846,6 @@
26E038821D8F94030006B265 /* Colours.swift in Sources */,
26C7FB241ED16D6F006B7101 /* AppDelegate.swift in Sources */,
26C7FB3B1ED16D6F006B7101 /* Point.swift in Sources */,
26C7FB231ED16D6F006B7101 /* POITableDataSource.swift in Sources */,
260586801F43F77400796399 /* ConfigProtocols.swift in Sources */,
26C7FB371ED16D6F006B7101 /* Arrival.swift in Sources */,
26517F541F803FE7001710FD /* OverlayButton.swift in Sources */,
Expand All @@ -862,6 +862,7 @@
26C7FB4C1ED16D6F006B7101 /* RailVehicle.swift in Sources */,
26C7FB4F1ED16D6F006B7101 /* StationTableViewCell.swift in Sources */,
260586841F43F97B00796399 /* SharedConfig.swift in Sources */,
26517F5D1F81DACF001710FD /* POITableDataSource.swift in Sources */,
26C7FB251ED16D6F006B7101 /* Box.swift in Sources */,
26C7FB4E1ED16D6F006B7101 /* BadgeViewLabel.swift in Sources */,
26C7FB301ED16D6F006B7101 /* MKCircle.swift in Sources */,
Expand All @@ -882,7 +883,6 @@
26C7FB211ED16D6F006B7101 /* RouteViewController.swift in Sources */,
26C7FB2C1ED16D6F006B7101 /* Array.swift in Sources */,
26C7FB2A1ED16D6F006B7101 /* ProperViewController.swift in Sources */,
26C7FB401ED16D6F006B7101 /* ArrivalsViewModel.swift in Sources */,
26C7FB271ED16D6F006B7101 /* Matchers.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
50 changes: 27 additions & 23 deletions Proper/Controllers/POIMapViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import UIKit
import ReactiveSwift
import MapKit
import Result
import Dwifft

class POIMapViewController: UIViewController, ProperViewController {
typealias DisposableType = ScopedDisposable<CompositeDisposable>

var map: MKMapView { return self.view as! MKMapView }
let onSelect: Action<MutableStation, (), NoError>
let routes: Property<Set<MutableRoute>>
let stations: Property<[MutableStation]>

// Mutable properties that can be set by the map to impact the POI search region.
let center: MutableProperty<Point>
Expand All @@ -28,25 +30,31 @@ class POIMapViewController: UIViewController, ProperViewController {
fileprivate var polylines = [MutableRoute: MKPolyline]()
fileprivate var routeForPolyline = [MKPolyline: MutableRoute]()
fileprivate let updateRegionLock = NSLock()
fileprivate var viewDidLayout = false

var connection: ConnectionType
var disposable = ScopedDisposable(CompositeDisposable())

init(center: MutableProperty<Point>,
zoom: MutableProperty<MKCoordinateSpan>,
routes: Property<Set<MutableRoute>>,
stations: Property<[MutableStation]>,
onSelect: Action<MutableStation, (), NoError>,
isUserLocation: Property<Bool>,
connection: ConnectionType = Connection.cachedInstance)
{
self.center = center
self.zoom = zoom
self.routes = routes
self.stations = stations
self.onSelect = onSelect
self.isUserLocation = isUserLocation
self.connection = connection
super.init(nibName: nil, bundle: nil)

map.translatesAutoresizingMaskIntoConstraints = false
map.region = MKCoordinateRegion(center: CLLocationCoordinate2D(point: center.value),
span: zoom.value)
map.delegate = self
}

Expand All @@ -56,7 +64,6 @@ class POIMapViewController: UIViewController, ProperViewController {

override func loadView() {
view = MKMapView()
view.translatesAutoresizingMaskIntoConstraints = false
}

// MARK: Lifecycle
Expand All @@ -71,9 +78,8 @@ class POIMapViewController: UIViewController, ProperViewController {

// Update the map as the center and zoom level changes. Center is expected to change following device location.
let coordinate = CLLocationCoordinate2D(point: point)
self.map.setCenter(coordinate, animated: true)
let boundingRegion = MKCoordinateRegion(center: coordinate, span: zoom)
self.map.setRegion(self.map.regionThatFits(boundingRegion), animated: true)
self.map.setRegion(boundingRegion, animated: true)
self.staticCenter?.coordinate = coordinate
self.updateRegionLock.unlock()
}
Expand All @@ -96,6 +102,18 @@ class POIMapViewController: UIViewController, ProperViewController {
}
}

disposable += stations.producer.combinePrevious([]).startWithValues { prev, next in
let diff = Dwifft.diff(prev, next)
for step in diff {
switch step {
case let .insert(idx, station):
self.addAnnotation(for: station, at: idx)
case let .delete(_, station):
self.deleteAnnotations(for: station)
}
}
}

disposable += routes.producer.flatMap(.latest, transform: { routes -> SignalProducer<Void, NoError> in
// Show route polyline annotations, updating as the routes change. The parent POIViewController will only
// provide routes inside the current search region.
Expand All @@ -110,22 +128,12 @@ class POIMapViewController: UIViewController, ProperViewController {
super.viewDidDisappear(animated)
}

// MARK: Map annotations

func modifyMap(with ops: [POIViewModel.Op]) {
for op in ops {
switch op {
case let .addStation(station, index: idx):
addAnnotation(for: station, at: idx)
case let .deleteStation(station, at: _):
deleteAnnotations(for: station)
case let .reorderStation(_, from: fi, to: ti):
reorderAnnotations(withIndex: fi, to: ti)
default: continue
}
}
override func viewDidLayoutSubviews() {
viewDidLayout = true
}

// MARK: Map annotations

func polyline(for route: MutableRoute) -> SignalProducer<Void, NoError> {
let producer = route.canonical.producer.skipNil().map({ route -> MKPolyline in
let points = route.stations.map({ stop in stop.station.position.value })
Expand Down Expand Up @@ -236,20 +244,16 @@ extension POIMapViewController: MKMapViewDelegate {

if let annotation = (view as? POIStationAnnotationView)?.annotation as? POIStationAnnotation {
disposable += onSelect.apply(annotation.station).start()
// let section = table.dataSource.index(of: annotation.station)
// let row = (table.dataSource.arrivals[section].isEmpty) ? NSNotFound : 0
// table.tableView.scrollToRow(at: IndexPath(row: row, section: section), at: .top,
// animated: true)
}
}

func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
guard updateRegionLock.try() else {
guard viewDidLayout, updateRegionLock.try() else {
return
}
// TODO: I shouldn't update the center point, because that's based on the nearby view's representative location.
// But I should make sure whatever region shown by the map is being searched.
// center.swap(Point(coordinate: mapView.region.center))
center.swap(Point(coordinate: mapView.region.center))
zoom.swap(mapView.region.span)
updateRegionLock.unlock()
}
Expand Down
106 changes: 48 additions & 58 deletions Proper/Controllers/POITableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,27 @@ import ReactiveSwift
import Result
import Curry
import CoreLocation
import Dwifft

class POITableViewController: UITableViewController, ProperViewController {
typealias Distance = CLLocationDistance
static let headerViewHeight = CGFloat(55)

var mapPoint: Property<Point>
let dataSource = POITableDataSource()
var connection: ConnectionType = Connection.cachedInstance
var disposable = CompositeDisposable()

internal var connection: ConnectionType = Connection.cachedInstance
internal var disposable = CompositeDisposable()
fileprivate var headerDisposables: [UIView: Disposable] = [:]
fileprivate var headerBadges: [UIView: Badge] = [:]
fileprivate let stations: Property<[MutableStation]>

static let headerViewHeight = CGFloat(55)
lazy var dataSource: POITableDataSource = {
return POITableDataSource(tableView: self.tableView, stations: Property(self.stations), connection: self.connection)
}()

init(style: UITableViewStyle, mapPoint: Property<Point>) {
init(style: UITableViewStyle, stations: Property<[MutableStation]>, mapPoint: Property<Point>) {
self.mapPoint = mapPoint
self.stations = stations
super.init(style: style)

tableView.translatesAutoresizingMaskIntoConstraints = false
Expand All @@ -39,61 +46,26 @@ class POITableViewController: UITableViewController, ProperViewController {
fatalError("init(coder:) has not been implemented")
}

/// Manipulates the table and data source with changes described by the table operations.
func modifyTable(with ops: [POIViewModel.Op]) {
// Iterate through `ops` and record changes made.
var sectionInsertions = IndexSet()
var sectionDeletions = IndexSet()
var rowInsertions = [IndexPath]()
var rowDeletions = [IndexPath]()

tableView.beginUpdates()
// Manipulate the data source for each operation.
for op in ops {
switch op {
case let .addStation(station, index: idx):
let badge = Badge(alphabetIndex: idx, seedForColor: station)
dataSource.insert(entry: (station, badge, []), at: idx)
sectionInsertions.insert(idx)

case let .addArrival(arrival, to: station):
let path = dataSource.indexPath(inserting: arrival, onto: station)
rowInsertions.append(path)

case let .deleteArrival(arrival, from: station):
guard let si = dataSource.stations.index(of: station),
dataSource.arrivals[si].index(of: arrival) != nil else {
NSLog("WARN: .deleteArrival received for an arrival that doesn't exist 😕")
continue
}
let path = dataSource.indexPath(deleting: arrival, from: station)
rowDeletions.append(path)

case let .deleteStation(station, at: idx):
dataSource.remove(station: station)
sectionDeletions.insert(idx)

case let .reorderStation(_, from: fi, to: ti):
dataSource.moveStation(from: fi, to: ti)
tableView.moveSection(fi, toSection: ti)
}
func scroll(to station: MutableStation) {
guard let index = dataSource.index(of: station) else {
return
}

let deleted = sectionDeletions.subtracting(sectionInsertions)
let inserted = sectionInsertions.subtracting(sectionDeletions)
let reloaded = sectionDeletions.intersection(sectionInsertions)

// Apply changes to the table.
tableView.deleteRows(at: rowDeletions, with: .top)
tableView.deleteSections(deleted, with: .automatic)
tableView.insertSections(inserted, with: .automatic)
tableView.reloadSections(reloaded, with: .automatic)
tableView.insertRows(at: rowInsertions, with: .bottom)
tableView.endUpdates()
let row = dataSource.arrivals(atSection: index).isEmpty ? NSNotFound : 0
tableView.scrollToRow(at: IndexPath(row: row, section: index), at: .top, animated: true)
}

// MARK: Lifecycle
fileprivate func resetBadgeIndices(startingFrom start: Int) {
for idx in stride(from: start, to: tableView.numberOfSections, by: 1) {
guard let view = tableView.headerView(forSection: idx) else {
continue
}
headerBadges[view]?.set(numericalIndex: idx)
}
}
}

// MARK: Lifecycle
extension POITableViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
Expand All @@ -103,8 +75,10 @@ class POITableViewController: UITableViewController, ProperViewController {
disposable = .init()
super.viewDidDisappear(animated)
}
}

// MARK: Table View Delegate
// MARK: Table View Delegate
extension POITableViewController {
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return POIViewModel.arrivalRowHeight
}
Expand All @@ -118,15 +92,31 @@ class POITableViewController: UITableViewController, ProperViewController {
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "stationHeader")
as! POIStationHeaderFooterView
let (station, badge, _) = dataSource.table[section]
let station = dataSource.station(at: section)
let badge = Badge(alphabetIndex: section, seedForColor: station)
let position = station.position.producer.skipNil()
let distance = POIViewModel.distanceString(mapPoint.producer.combineLatest(with: position))

header.apply(station: station, badge: badge, distance: distance)

headerBadges[header] = badge
return header
}

override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return POITableViewController.headerViewHeight
}

override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
resetBadgeIndices(startingFrom: section+1)
let station = dataSource.station(at: section)
let disposable = dataSource.fetchArrivals(for: station).startWithFailed(displayError(_:))
headerDisposables[view] = disposable
}

override func tableView(_ tableView: UITableView, didEndDisplayingHeaderView view: UIView, forSection section: Int) {
headerBadges[view] = nil
resetBadgeIndices(startingFrom: section+1)
headerDisposables.removeValue(forKey: view)?.dispose()
}
}
Loading

0 comments on commit 70a918e

Please sign in to comment.