Files
2024-06-23 16:24:31 +02:00

373 lines
13 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 = EQNBaseMapFilter.oneDay {
didSet {
updateUI()
}
}
/// Available filters
var availableFilters: [EQNBaseMapFilter] {
[.oneDay, .twelveHours, .sixHours, .twoHours, .oneHour, .tenMinutes]
}
/// If `true`, the filter view is visible. Could be overridden by a subclass with a custom filter view
var isFilterViewVisible: Bool {
!availableFilters.isEmpty
}
/// If `true` the close button will be shown on top of the map view
var isCloseButtonVisible: Bool {
true
}
// MARK: - Internal
/// Annotations displayed on the map
private(set) 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
}()
// app icon and name displayed on the screenshot
private lazy var watermarkView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
let logo = UIImageView(image: .init(named: "eq_icon_transparent"))
logo.translatesAutoresizingMaskIntoConstraints = false
logo.contentMode = .scaleAspectFit
view.addSubview(logo)
logo.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
logo.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
logo.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
logo.widthAnchor.constraint(equalTo: logo.heightAnchor).isActive = true
let title = UILabel()
title.translatesAutoresizingMaskIntoConstraints = false
title.text = NSLocalizedString("app_name", comment: "") + " App"
title.textColor = AppTheme.Colors.red
title.font = .preferredFont(forTextStyle: .title3, weight: .semibold)
view.addSubview(title)
title.leadingAnchor.constraint(equalTo: logo.trailingAnchor, constant: 10.0).isActive = true
title.centerYAnchor.constraint(equalTo: logo.centerYAnchor).isActive = true
title.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
return view
}()
// 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(containerView)
if isFilterViewVisible {
view.addSubview(filtersView)
}
if isCloseButtonVisible {
view.addSubview(closeButton)
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
extraUI()
}
private func addWatermarkView() {
view.addSubview(watermarkView)
watermarkView.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 10.0).isActive = true
watermarkView.leadingAnchor.constraint(equalTo: mapView.leadingAnchor, constant: 10.0).isActive = true
watermarkView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
}
private func removeWatermarkView() {
watermarkView.removeFromSuperview()
}
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
registerMapAnnotationViews()
loadDataSource()
}
override func didReceiveDownloadComplete(_ notification: Notification) {
super.didReceiveDownloadComplete(notification)
// when data is download, reload data source
DispatchQueue.main.async {
self.loadDataSource()
}
}
// MARK: - Public
/// Add extra UI not available in the base class
func extraUI() {
// nope, subclass will implement some logic
}
/// Configure UI after view initialization
func configureUI() {
// nope, subclass will implement some logic
}
/// 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()
}
func reloadMap() {
// remove and re-add annotations, to force redrawn
mapView.removeAnnotations(mapAnnotations)
mapView.addAnnotations(mapAnnotations)
}
/// 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
}
func zPriority(for annotation: MKAnnotation) -> MKAnnotationViewZPriority {
// subclass will impelement its own logic to define annotation priority
.min
}
func createSnapshot(
prepare: () -> Void = { },
restore: () -> Void = { }
) -> UIImage {
prepare()
addWatermarkView()
// riduciamo la porzione da salvare alla sola mappa (eliminiamo i filtri)
let size = CGSize(width: view.bounds.width, height: mapView.bounds.maxY)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { ctx in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
restore()
removeWatermarkView()
return image
}
// 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("status_cancel", comment: ""), style: .cancel, handler: nil))
present(sheet, animated: true, completion: nil)
}
private func applyFilter(_ filter: EQNBaseMapFilter) {
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)
annotationView?.zPriority = zPriority(for: annotation)
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)
}
}