// // 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) } }