282 lines
9.9 KiB
Swift
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)
|
|
}
|
|
}
|