Files
eqn.ios/Sources/Earthquake Network/Controllers/Shared/EQNBaseMapViewController.swift
T
2021-03-23 19:12:39 +01:00

282 lines
9.9 KiB
Swift

//
// EQNBaseMapViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 06/03/21.
// Copyright © 2021 Earthquake Network. All rights reserved.
//
import Foundation
import MapKit
class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
/// Current filter
var filter = EQNFiltroMappa.unGiorno {
didSet {
updateUI()
}
}
/// Available filters
var availableFilters: [EQNFiltroMappa] {
[.unGiorno, .dodiciOre, .seiOre, .dueOre, .unOra, .dieciMinuti]
}
/// If `true`, the filter view is visible. Could be overridden by a subclass with a custom filter view
var isFilterViewVisible: Bool {
!availableFilters.isEmpty
}
// MARK: - Internal
/// Annotations displayed on the map
private var mapAnnotations = [MKAnnotation]()
/// If `true`, the initial filter has been already evaluated
private var initialFilterEvaluated = false
// MARK: - UI
lazy var closeButton: EQNBlurredCloseButton = {
let button = EQNBlurredCloseButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(closeTapped(_:)), for: .touchUpInside)
button.setTitleColor(.darkGray, for: .normal)
return button
}()
lazy var mapView: MKMapView = {
let mapView = MKMapView()
mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.showsUserLocation = true
mapView.delegate = self
return mapView
}()
private lazy var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override var bannerContainerView: UIView? {
return containerView
}
lazy var filtersView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = AppTheme.Colors.lightGray
// label with current selecte filter
view.addSubview(filterLabel)
filterLabel.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
filterLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
filterLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8.0).isActive = true
let imageView = UIImageView(image: UIImage(named: "icon-arrow-down"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.tintColor = .black
imageView.contentMode = .scaleAspectFit
view.addSubview(imageView)
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0).isActive = true
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8.0).isActive = true
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8.0).isActive = true
imageView.leadingAnchor.constraint(equalTo: filterLabel.trailingAnchor, constant: 8.0).isActive = true
// tap recognizer
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(filtersTapped(_:)))
view.addGestureRecognizer(tapRecognizer)
return view
}()
private lazy var filterLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = filter.title
return label
}()
// MARK: - Init
init() {
super.init(nibName: nil, bundle: nil)
setupUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
// MARK: - Private
private func setupUI() {
view.backgroundColor = .white
view.addSubview(mapView)
view.addSubview(closeButton)
view.addSubview(containerView)
if isFilterViewVisible {
view.addSubview(filtersView)
}
closeButton.addDefaultConstraint(to: view)
containerView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
bannerContainerHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: 55.0)
bannerContainerHeightConstraint?.isActive = true
if isFilterViewVisible {
filtersView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
filtersView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
filtersView.bottomAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
filtersView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
}
if isFilterViewVisible && !isBannerVisible {
view.backgroundColor = filtersView.backgroundColor // trick to simulate a bigger filters view
}
mapView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
mapView.bottomAnchor.constraint(equalTo: (isFilterViewVisible ? filtersView : containerView).topAnchor).isActive = true
}
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
registerMapAnnotationViews()
loadDataSource()
}
override func didReceiveDownloadComplete(_ notification: Notification) {
super.didReceiveDownloadComplete(notification)
// when data is download, reload data source
DispatchQueue.main.async {
self.loadDataSource()
}
}
// MARK: - Public
/// Load data to display on the map
func loadDataSource() {
// nope, subclass will implement some logic
}
/// Register annotation views for the current map
func registerMapAnnotationViews() {
// nope, subclass will implement some logic
}
func elaborateMapCenter() {
// nope, subclass will center map with it's own logic
}
func setDefaultFilter(for items: [EQNBaseMapRepresentable]) {
// initial filter will be the upper nearest of the newest item
// this logic has to be evaluated only one time, to keep user filter change working
guard !initialFilterEvaluated, let newest = items.sorted(by: { $0.date > $1.date }).first else {
return
}
// find the filter that include the newest item
let difference = Date().timeIntervalSince(newest.date) / 60.0
let betterFilter = availableFilters
.filter { $0.minutes > difference }
.sorted()
.first
// use the filter found, or the first available
if let betterFilter = betterFilter {
filter = betterFilter
} else if let firstFilter = availableFilters.first {
filter = firstFilter
}
initialFilterEvaluated = true
}
/// Update the map with a set of annotations
func updateMap(with annotations: [MKAnnotation]) {
// remove previous annotations
mapView.removeAnnotations(mapAnnotations)
// set new annotations and reload map
mapAnnotations = annotations
mapView.addAnnotations(mapAnnotations)
mapView.showAnnotations(mapAnnotations, animated: true)
elaborateMapCenter()
}
/// Changes the center coordinate of the map to a given location
func setMapCenter(for location: CLLocation, span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 8, longitudeDelta: 8)) {
let region = MKCoordinateRegion(center: location.coordinate, span: span)
mapView.setCenter(location.coordinate, animated: false)
mapView.setRegion(region, animated: true)
}
func didTapAnnotation(_ annotation: MKAnnotation) {
// nope, subclass will implement logic
}
// MARK: - Private
private func updateUI() {
filterLabel.text = filter.title
}
// MARK: - Actions
@objc func closeTapped(_ sender: Any) {
dismiss(animated: true)
}
@objc func filtersTapped(_ sender: UIGestureRecognizer) {
let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
availableFilters.forEach { (filter) in
sheet.addAction(UIAlertAction(title: filter.title, style: .default, handler: { _ in
self.applyFilter(filter)
}))
}
sheet.addAction(UIAlertAction(title: NSLocalizedString("Annulla", comment: ""), style: .cancel, handler: nil))
present(sheet, animated: true, completion: nil)
}
private func applyFilter(_ filter: EQNFiltroMappa) {
self.filter = filter
loadDataSource()
}
// MARK: - MKMapViewDelegate
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !annotation.isKind(of: MKUserLocation.self) else {
// Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize.
return nil
}
let annotationView = setupAnnotationView(for: annotation, on: mapView)
return annotationView
}
func setupAnnotationView(for annotation: MKAnnotation, on mapView: MKMapView) -> MKAnnotationView? {
return nil
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let annotation = view.annotation else { return }
didTapAnnotation(annotation)
}
}