// // 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) mapView.deselectAnnotation(annotation, animated: true) } }