/
MapViewController.swift
202 lines (170 loc) Β· 7.18 KB
/
MapViewController.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//
// MapViewController.swift
// CriticalMaps
//
// Created by Leonard Thomas on 1/18/19.
//
import MapKit
import UIKit
class MapViewController: UIViewController {
class IdentifiableAnnnotation: MKPointAnnotation {
var identifier: String
var location: Location {
set {
coordinate = CLLocationCoordinate2D(latitude: newValue.latitude, longitude: newValue.longitude)
}
@available(*, unavailable)
get {
fatalError("Not implemented")
}
}
init(location: Location, identifier: String) {
self.identifier = identifier
super.init()
self.location = location
}
}
// MARK: Properties
private let nightThemeOverlay = DarkModeMapOverlay()
public lazy var followMeButton: UserTrackingButton = {
let button = UserTrackingButton(mapView: mapView)
return button
}()
public var bottomContentOffset: CGFloat = 0 {
didSet {
mapView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: bottomContentOffset, right: 0)
}
}
private var mapView: MKMapView {
return view as! MKMapView
}
private let gpsDisabledOverlayView: UIVisualEffectView = {
let view = UIVisualEffectView()
view.accessibilityViewIsModal = true
view.effect = UIBlurEffect(style: .light)
let label = NoContentMessageLabel()
label.text = NSLocalizedString("map.layer.info", comment: "")
label.numberOfLines = 0
label.textAlignment = .center
label.sizeToFit()
view.contentView.addSubview(label)
label.center = view.center
label.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin, .flexibleBottomMargin]
return view
}()
private let themeController: ThemeController!
private var tileRenderer: MKTileOverlayRenderer?
init(themeController: ThemeController) {
self.themeController = themeController
super.init(nibName: nil, bundle: nil)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("map.title", comment: "")
configureNotifications()
configureTileRenderer()
configureMapView()
condfigureGPSDisabledOverlayView()
setNeedsStatusBarAppearanceUpdate()
}
override func loadView() {
view = MKMapView(frame: .zero)
}
private func configureTileRenderer() {
guard themeController.currentTheme == .dark else {
return
}
tileRenderer = MKTileOverlayRenderer(tileOverlay: nightThemeOverlay)
mapView.addOverlay(nightThemeOverlay, level: .aboveLabels)
}
private func condfigureGPSDisabledOverlayView() {
view.addSubview(gpsDisabledOverlayView)
gpsDisabledOverlayView.frame = view.bounds
gpsDisabledOverlayView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
updateGPSDisabledOverlayVisibility()
}
private func configureNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(positionsDidChange(notification:)), name: Notification.positionOthersChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didReceiveInitialLocation(notification:)), name: Notification.initialGpsDataReceived, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(updateGPSDisabledOverlayVisibility), name: Notification.gpsStateChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: Notification.themeDidChange, object: nil)
}
private func configureMapView() {
if #available(iOS 11.0, *) {
mapView.register(BikeAnnoationView.self, forAnnotationViewWithReuseIdentifier: BikeAnnoationView.identifier)
}
mapView.showsPointsOfInterest = false
mapView.delegate = self
mapView.showsUserLocation = true
}
private func display(locations: [String: Location]) {
var unmatchedLocations = locations
var unmatchedAnnotations: [MKAnnotation] = []
// update existing annotations
mapView.annotations.compactMap { $0 as? IdentifiableAnnnotation }.forEach { annotation in
if let location = unmatchedLocations[annotation.identifier] {
annotation.location = location
unmatchedLocations.removeValue(forKey: annotation.identifier)
} else {
unmatchedAnnotations.append(annotation)
}
}
let annotations = unmatchedLocations.map { IdentifiableAnnnotation(location: $0.value, identifier: $0.key) }
mapView.addAnnotations(annotations)
// remove annotations that no longer exist
mapView.removeAnnotations(unmatchedAnnotations)
}
@objc func updateGPSDisabledOverlayVisibility() {
gpsDisabledOverlayView.isHidden = LocationManager.accessPermission == .authorized
}
// MARK: Notifications
override var preferredStatusBarStyle: UIStatusBarStyle {
return themeController.currentTheme.style.statusBarStyle
}
@objc private func themeDidChange() {
let theme = themeController.currentTheme
guard theme == .dark else {
tileRenderer = nil
mapView.removeOverlay(nightThemeOverlay)
return
}
configureTileRenderer()
}
@objc private func positionsDidChange(notification: Notification) {
guard let response = notification.object as? ApiResponse else { return }
display(locations: response.locations)
}
@objc func didReceiveInitialLocation(notification: Notification) {
guard let location = notification.object as? Location else { return }
let region = MKCoordinateRegion(center: CLLocationCoordinate2D(location), latitudinalMeters: 10000, longitudinalMeters: 10000)
let adjustedRegion = mapView.regionThatFits(region)
mapView.setRegion(adjustedRegion, animated: true)
}
}
extension MapViewController: MKMapViewDelegate {
// MARK: MKMapViewDelegate
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard annotation is MKUserLocation == false else {
return nil
}
if #available(iOS 11.0, *) {
return mapView.dequeueReusableAnnotationView(withIdentifier: BikeAnnoationView.identifier, for: annotation)
} else {
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: BikeAnnoationView.identifier) ?? BikeAnnoationView()
annotationView.annotation = annotation
return annotationView
}
}
func mapView(_: MKMapView, didChange mode: MKUserTrackingMode, animated _: Bool) {
followMeButton.currentMode = UserTrackingButton.Mode(mode)
}
func mapView(_: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
guard let renderer = self.tileRenderer else {
return MKOverlayRenderer(overlay: overlay)
}
return renderer
}
}