feat: Add new realtime alert screen

This commit is contained in:
Andrea Busi
2022-06-15 17:25:35 +02:00
parent 11d994696d
commit 2b8f2db7c5
5 changed files with 404 additions and 11 deletions
@@ -131,6 +131,8 @@
6562C80725FFA6B100C85273 /* SeismicNetworkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6562C80625FFA6B100C85273 /* SeismicNetworkViewModel.swift */; };
6586971125F44C26009C0182 /* EQNBlurredCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6586971025F44C26009C0182 /* EQNBlurredCloseButton.swift */; };
658BAB7B25FE67930015C454 /* EQNBaseMapRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658BAB7A25FE67930015C454 /* EQNBaseMapRepresentable.swift */; };
658BC0292859A456009EECAA /* RealtimeAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658BC0282859A456009EECAA /* RealtimeAlertViewController.swift */; };
658BC02B2859A4D3009EECAA /* RealtimeAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658BC02A2859A4D3009EECAA /* RealtimeAlertView.swift */; };
65AD23CE261B03D400E3B57C /* SubscriptionsDescriptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AD23CD261B03D400E3B57C /* SubscriptionsDescriptionTableViewCell.swift */; };
65BBB22C26064BE6005D6CDF /* SegnalazioniLast24HoursCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65BBB22B26064BE6005D6CDF /* SegnalazioniLast24HoursCell.swift */; };
65D409942619BA34008CF356 /* SegnalazioniSendReportCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65D409932619BA34008CF356 /* SegnalazioniSendReportCell.swift */; };
@@ -407,6 +409,8 @@
6562C80625FFA6B100C85273 /* SeismicNetworkViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeismicNetworkViewModel.swift; sourceTree = "<group>"; };
6586971025F44C26009C0182 /* EQNBlurredCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNBlurredCloseButton.swift; sourceTree = "<group>"; };
658BAB7A25FE67930015C454 /* EQNBaseMapRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNBaseMapRepresentable.swift; sourceTree = "<group>"; };
658BC0282859A456009EECAA /* RealtimeAlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealtimeAlertViewController.swift; sourceTree = "<group>"; };
658BC02A2859A4D3009EECAA /* RealtimeAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealtimeAlertView.swift; sourceTree = "<group>"; };
65A4D5AA26280A24003918E0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
65A4D5AB26280A24003918E0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
65A4D5AC26280A56003918E0 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@@ -617,6 +621,15 @@
path = Debug;
sourceTree = "<group>";
};
658BC0272859A43C009EECAA /* Realtime Alert */ = {
isa = PBXGroup;
children = (
658BC0282859A456009EECAA /* RealtimeAlertViewController.swift */,
658BC02A2859A4D3009EECAA /* RealtimeAlertView.swift */,
);
path = "Realtime Alert";
sourceTree = "<group>";
};
65DBFB5225E2A2580041CBA6 /* Map annotation */ = {
isa = PBXGroup;
children = (
@@ -1090,6 +1103,7 @@
DCEFF21024F57163009D3FE1 /* Settings */,
DC141968250E769B0059E060 /* Seismic Networks */,
DCB5281F256015EB005288E5 /* Simulator */,
658BC0272859A43C009EECAA /* Realtime Alert */,
);
path = Controllers;
sourceTree = "<group>";
@@ -1580,6 +1594,7 @@
DCF9E14D24F6D1AA002B6B1D /* EQNData.swift in Sources */,
DC52B8A524FCCD6900ABEBA6 /* AppTheme.swift in Sources */,
653C680425F3DF8A00FE52AC /* EQNBaseMapViewController.swift in Sources */,
658BC02B2859A4D3009EECAA /* RealtimeAlertView.swift in Sources */,
DC27EB2F24F6EBE000ACBFE0 /* SettingsSeismicNetworksViewController.swift in Sources */,
8CF66059214C566B009F4314 /* Reachability.m in Sources */,
DC886A5D24E92D5500F7A5D3 /* EQNBaseViewController.m in Sources */,
@@ -1611,6 +1626,7 @@
DC65B391250F243E00251693 /* SeismicSettingsNetworksViewController.swift in Sources */,
65DBFB4B25E29DD60041CBA6 /* SeismicNetworksMapDetailViewController.swift in Sources */,
DC08804124F5B41400186D97 /* SettingSliderTableViewCell.swift in Sources */,
658BC0292859A456009EECAA /* RealtimeAlertViewController.swift in Sources */,
8CBD3DD82149B9AD0070C963 /* main.m in Sources */,
8CF05B57218C93BA0055012B /* EQNUtility.m in Sources */,
8C4E34422152B5E8008B0D2A /* EQNRilevamento.m in Sources */,
@@ -117,14 +117,19 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
self.expandeCollapseButton.image = showAllCards ? [UIImage imageNamed:@"navbar-icon-arrow-collapse"] : [UIImage imageNamed:@"navbar-icon-arrow-expand"];
NSDate *date = [[NSUserDefaults standardUserDefaults] objectForKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
if (date) {
if ([EQNUtility getDifferenceMinute:date] < TEMPO_VISUALIZZAZIONE_NOTIFICA)
self.isNotificaAttiva = YES;
else{
self.isNotificaAttiva = NO;
[[NSUserDefaults standardUserDefaults] removeObjectForKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
[[NSUserDefaults standardUserDefaults] synchronize];
if (date && [EQNUtility getDifferenceMinute:date] < TEMPO_VISUALIZZAZIONE_NOTIFICA) {
self.isNotificaAttiva = YES;
NSDictionary *info = [EQNUtility loadDictionaryFromUserDefaultsForKey:NOTIFICHE_RETE_SMARTPHONE_DIZIONARIO_NOTIFICA];
RealtimeAlertViewController *controller = [[RealtimeAlertViewController alloc] initWithNotification:info];
if (controller) {
if (@available(iOS 13.0, *)) {
controller.modalInPresentation = YES;
}
[self presentViewController:controller animated:YES completion:nil];
}
} else {
self.isNotificaAttiva = NO;
[[NSUserDefaults standardUserDefaults] removeObjectForKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
}
[self.tableItems removeAllObjects];
@@ -173,9 +178,8 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:NOTIFICHE_RETE_SMARTPHONE_DIZIONARIO_NOTIFICA];
[[NSUserDefaults standardUserDefaults] synchronize];
self.isNotificaAttiva = NO;
[self.tableView reloadData];
}
@@ -0,0 +1,160 @@
//
// RealtimeAlertView.swift
// Earthquake Network
//
// Created by Andrea Busi on 15/06/22.
// Copyright © 2022 Earthquake Network. All rights reserved.
//
import UIKit
import MapKit
class RealtimeAlertView: UIView {
let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .title2)
label.text = NSLocalizedString("app_name", comment: "")
return label
}()
let descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 0
return label
}()
let waveTimeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .title3)
label.textColor = AppTheme.Colors.red
label.text = String(format: NSLocalizedString("alert_wave", comment: ""), 0)
label.textAlignment = .center
label.isHidden = true
return label
}()
lazy var mapView: MKMapView = {
let map = MKMapView()
map.translatesAutoresizingMaskIntoConstraints = false
map.delegate = self
map.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
map.showsUserLocation = true
return map
}()
let closeButton: EQNBlurredCloseButton = {
let button = EQNBlurredCloseButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(.darkGray, for: .normal)
return button
}()
// MARK: - Init
convenience init() {
self.init(frame: .zero)
configureUI()
}
// MARK: - Private
private func configureUI() {
backgroundColor = AppTheme.Colors.lightGray
addSubview(closeButton)
addSubview(titleLabel)
addSubview(descriptionLabel)
addSubview(waveTimeLabel)
addSubview(mapView)
closeButton.addDefaultConstraint(to: self)
titleLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -10.0).isActive = true
titleLabel.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: 10.0).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor).isActive = true
descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20.0).isActive = true
waveTimeLabel.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor).isActive = true
waveTimeLabel.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
waveTimeLabel.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 20.0).isActive = true
mapView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
mapView.topAnchor.constraint(equalTo: waveTimeLabel.bottomAnchor, constant: 20.0).isActive = true
mapView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
// MARK: - Public
func addMapCircle(
center: CLLocationCoordinate2D,
radius: CLLocationDistance,
overlayId: String
) {
// remove any other existing overlays
let overlays = mapView.overlays.filter { $0.title == overlayId }
mapView.removeOverlays(overlays)
// add new overlay
let circle = MKCircle(center: center, radius: radius)
circle.title = overlayId
mapView.addOverlay(circle)
}
func addMapLine(
coordinates: [CLLocationCoordinate2D]
) {
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
mapView.addOverlay(polyline)
}
func addMapAnnotation(
title: String = "",
center: CLLocationCoordinate2D,
intensity: Int
) {
let annotation = EQNMapAnnotationPastquake(title: title, coordinate: center, intensity: intensity)
mapView.addAnnotation(annotation)
}
}
extension RealtimeAlertView: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
switch overlay {
case let circle as MKCircle:
let circleRenderer = MKCircleRenderer(overlay: circle)
circleRenderer.strokeColor = AppTheme.Colors.red
circleRenderer.fillColor = AppTheme.Colors.red.withAlphaComponent(0.2)
circleRenderer.lineWidth = 3.0
return circleRenderer
case let polyline as MKPolyline:
let polylineRenderer = MKPolylineRenderer(polyline: polyline)
polylineRenderer.strokeColor = .blue
polylineRenderer.lineWidth = 2.0
return polylineRenderer
default:
return MKOverlayRenderer(overlay: overlay)
}
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? EQNMapAnnotationPastquake else {
return nil
}
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: EQNCustomAnnotationView.SingleLineIdentifier, for: annotation) as! EQNCustomAnnotationView
annotationView.image = annotation.image
annotationView.title = annotation.title
return annotationView
}
}
@@ -0,0 +1,213 @@
//
// RealtimeAlertViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 15/06/22.
// Copyright © 2022 Earthquake Network. All rights reserved.
//
import UIKit
import MapKit
class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
@objc var onClose: () -> Void = {}
// MARK: - Internal
typealias NotificationPayload = [String: Any]
private let notificationView = RealtimeAlertView()
// complete push payload
private let notification: NotificationPayload
// aps.alert field of given push notification
private let alert: NotificationPayload
/// Coordinate of the earthquake
private let coordinate: CLLocation
/// Calculated timestamp for earthquake on user position
private let impactTimestamp: Date
/// Timer to constantly update countdown label
private var countdownTimer: Timer?
/// Refresh time for wave animation
private let waveAnimationRefreshRate = 0.1
/// Current radius of the wave animation on the map
private var waveAnimationCurrentRadius: CLLocationDistance = 0
private var waveAnimationVelocity: Double = 1_000
/// Timer to simulate animation for the wave
private var waveAnimationTimer: Timer?
private var currentCountdown: Int {
let now = Date()
let difference = lround(max(impactTimestamp.timeIntervalSince(now), 0))
return difference
}
// MARK: - Init
@objc
init?(notification: NotificationPayload) {
guard let alert = Self.getPushAlertPayload(from: notification),
let coordinate = Self.getCoordinate(from: notification),
let impactTimestamp = EQNUtility.calculateUserSeismicTimestamp(fromUserInfo: notification) else {
return nil
}
self.notification = notification
self.alert = alert
self.coordinate = coordinate
self.impactTimestamp = impactTimestamp
super.init(nibName: nil, bundle: nil)
self.waveAnimationCurrentRadius = currentWavePosition()
self.waveAnimationVelocity = evaluateWaveVelocity()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - View Lifecycle
override func loadView() {
view = notificationView
}
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
updateUI()
startCountdown()
startWaveAnimation()
}
// MARK: - Private
private func configureUI() {
notificationView.closeButton.addTarget(self, action: #selector(onTapClose(_:)), for: .touchUpInside)
}
private func updateUI() {
if let title = alert["loc-key"] as? String, let args = alert["loc-args"] as? [String], let arg = args.first {
notificationView.descriptionLabel.text = String(format: NSLocalizedString(title, comment: ""), arg)
}
// update title with distance from earthquake
let distance = EQNUser.default().lastPosition?.distance(from: coordinate) ?? 0.0
let distanceRound = Int(round(distance / 1_000))
notificationView.descriptionLabel.text = (notificationView.descriptionLabel.text ?? "")
+ ".\n"
+ String(format: NSLocalizedString("timer_message2_other", comment: ""), distanceRound)
// center map on the earthquake coordinate
let span = MKCoordinateSpan(latitudeDelta: 10.5, longitudeDelta: 10.5)
let region = MKCoordinateRegion(center: coordinate.coordinate, span: span)
notificationView.mapView.setCenter(coordinate.coordinate, animated: false)
notificationView.mapView.setRegion(region, animated: true)
// aggiungiamo annotation con epicentro sisma
let intensity = notification.eqn_intValue(for: "intensity") ?? 0
notificationView.addMapAnnotation(center: coordinate.coordinate, intensity: intensity)
// simuliamo animazione dell'onda sismica
notificationView.addMapCircle(center: coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation")
// aggiungiamo un segmento tra la posizione del sisma e quella dell'utente
if let lastPosition = EQNUser.default().lastPosition {
notificationView.addMapLine(coordinates: [coordinate.coordinate, lastPosition.coordinate])
}
}
private func startCountdown() {
// show countdown only if time is less than 300 seconds
if currentCountdown < 300 {
// start a timer for the countdown label
notificationView.waveTimeLabel.isHidden = false
countdownTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(countdownTimerFired(_:)), userInfo: nil, repeats: true)
countdownTimer?.fire()
}
}
private func startWaveAnimation() {
waveAnimationTimer = Timer.scheduledTimer(timeInterval: waveAnimationRefreshRate, target: self, selector: #selector(mapWaveAnimationFired(_:)), userInfo: nil, repeats: true)
waveAnimationTimer?.fire()
}
// MARK: - Action
@objc private func onTapClose(_ sender: UIButton) {
onClose()
dismiss(animated: true)
}
// MARK: - Timer
@objc private func countdownTimerFired(_ sender: Timer) {
notificationView.waveTimeLabel.text = String(format: NSLocalizedString("alert_wave", comment: ""), currentCountdown)
if currentCountdown <= 0 {
// stop the countdown
countdownTimer?.invalidate()
countdownTimer = nil
}
}
@objc private func mapWaveAnimationFired(_ sender: Timer) {
waveAnimationCurrentRadius += waveAnimationVelocity
notificationView.addMapCircle(center: coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation")
}
// MARK: - Helpers
/// Retrieve coordinate of earthquake from the notification payload
/// - Parameter notification: Notification payload
/// - Returns: Coordinate if found, nil otherwise
static func getCoordinate(
from notification: NotificationPayload
) -> CLLocation? {
guard let latitude = notification.eqn_doubleValue(for: "latitude"),
let longitude = notification.eqn_doubleValue(for: "longitude") else {
return nil
}
return CLLocation(latitude: latitude, longitude: longitude)
}
/// Get `aps.alert` object inside a given notification payload
/// - Parameter notification: Notification payload
/// - Returns: `aps.alert` object if found, nil otherwise
static func getPushAlertPayload(
from notification: NotificationPayload
) -> NotificationPayload? {
guard let aps = notification["aps"] as? [String: Any],
let alert = aps["alert"] as? [String: Any] else {
return nil
}
return alert
}
/// Evaluate current position for the wave
/// Used to define initial position for the wave circle
/// - Returns: Distance of the wave from the original earthquake point
private func currentWavePosition() -> Double {
// velocità onda
let waveSpeed = (notification.eqn_doubleValue(for: "wave_speed") ?? 0) * 1000 // m/s
// distanza tra utente e terremoto
let distance = EQNUser.default().lastPosition?.distance(from: coordinate) ?? 0.0 // m
// calcoliamo la distanza rimanente da mostrare, perchè la schermata potrebbe anche essere aperta in ritardo
let remainingDistance = waveSpeed * Double(currentCountdown)
return distance - remainingDistance
}
/// Evaluate wave velocity based on push notification data
/// - Returns: Wave velocity, used for animation
private func evaluateWaveVelocity() -> Double {
let waveSpeed = notification.eqn_doubleValue(for: "wave_speed") ?? 0 // km/s
let velocity = waveSpeed * 1000
return velocity * waveAnimationRefreshRate
}
}
@@ -61,9 +61,9 @@ class EQNBlurredCloseButton: UIButton {
/// Add constaints to show the button on the upper right corner
func addDefaultConstraint(to view: UIView) {
leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10.0).isActive = true
trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10.0).isActive = true
topAnchor.constraint(equalTo: view.topAnchor, constant: 10.0).isActive = true
widthAnchor.constraint(equalTo: heightAnchor).isActive = true
heightAnchor.constraint(equalToConstant: 40.0).isActive = true
heightAnchor.constraint(equalToConstant: 34.0).isActive = true
}
}