From 63592e6cfbbe4f7ee31957e8bfecccef8b78fbd4 Mon Sep 17 00:00:00 2001 From: Andrea Busi Date: Fri, 17 Jun 2022 12:49:57 +0200 Subject: [PATCH] refactor: Create a model for the realtime alert --- .../project.pbxproj | 4 + .../Alerts/AllerteViewController.m | 5 +- .../RealtimeAlertViewController.swift | 100 ++++-------------- .../Models/EQNRealtimeAlert.swift | 91 ++++++++++++++++ 4 files changed, 120 insertions(+), 80 deletions(-) create mode 100644 Sources/Earthquake Network/Models/EQNRealtimeAlert.swift diff --git a/Sources/Earthquake Network.xcodeproj/project.pbxproj b/Sources/Earthquake Network.xcodeproj/project.pbxproj index 86990de..7885ba3 100644 --- a/Sources/Earthquake Network.xcodeproj/project.pbxproj +++ b/Sources/Earthquake Network.xcodeproj/project.pbxproj @@ -140,6 +140,7 @@ 65DBFB7425E2BBF20041CBA6 /* GADTMediumTemplateView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 65DBFB6F25E2BBF20041CBA6 /* GADTMediumTemplateView.xib */; }; 65DBFB7525E2BBF20041CBA6 /* GADTMediumTemplateView.m in Sources */ = {isa = PBXBuildFile; fileRef = 65DBFB7125E2BBF20041CBA6 /* GADTMediumTemplateView.m */; }; 65DBFB7625E2BBF20041CBA6 /* GADTTemplateView.m in Sources */ = {isa = PBXBuildFile; fileRef = 65DBFB7225E2BBF20041CBA6 /* GADTTemplateView.m */; }; + 65E1226C285C92AA000E294A /* EQNRealtimeAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E1226B285C92AA000E294A /* EQNRealtimeAlert.swift */; }; 65E1B19B260F980600A0ACBA /* Dictionary+EQNExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E1B19A260F980600A0ACBA /* Dictionary+EQNExtensions.swift */; }; 8C10B0B92281FE7F00125C9F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8C10B0BD2281FE7F00125C9F /* Localizable.strings */; }; 8C10B0BA2281FE7F00125C9F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8C10B0BD2281FE7F00125C9F /* Localizable.strings */; }; @@ -430,6 +431,7 @@ 65DBFB7125E2BBF20041CBA6 /* GADTMediumTemplateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GADTMediumTemplateView.m; sourceTree = ""; }; 65DBFB7225E2BBF20041CBA6 /* GADTTemplateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GADTTemplateView.m; sourceTree = ""; }; 65DBFB7325E2BBF20041CBA6 /* GADTMediumTemplateView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GADTMediumTemplateView.h; sourceTree = ""; }; + 65E1226B285C92AA000E294A /* EQNRealtimeAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNRealtimeAlert.swift; sourceTree = ""; }; 65E1B19A260F980600A0ACBA /* Dictionary+EQNExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+EQNExtensions.swift"; sourceTree = ""; }; 8C10B0BC2281FE7F00125C9F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 8C10B0BE2281FE9E00125C9F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -1084,6 +1086,7 @@ 8C593E89217BA2470008B260 /* EQNSegnalazione.m */, 8CF4F4D9216D44930057110B /* EQNPastquakes.h */, 8CF4F4DA216D44930057110B /* EQNPastquakes.m */, + 65E1226B285C92AA000E294A /* EQNRealtimeAlert.swift */, ); path = Models; sourceTree = ""; @@ -1592,6 +1595,7 @@ DCA5B6E7252E4BD8002AEC96 /* EQNBaseTableViewCell.swift in Sources */, 8CF66058214C566B009F4314 /* ServerRequest.m in Sources */, DCF9E14D24F6D1AA002B6B1D /* EQNData.swift in Sources */, + 65E1226C285C92AA000E294A /* EQNRealtimeAlert.swift in Sources */, DC52B8A524FCCD6900ABEBA6 /* AppTheme.swift in Sources */, 653C680425F3DF8A00FE52AC /* EQNBaseMapViewController.swift in Sources */, 658BC02B2859A4D3009EECAA /* RealtimeAlertView.swift in Sources */, diff --git a/Sources/Earthquake Network/Controllers/Alerts/AllerteViewController.m b/Sources/Earthquake Network/Controllers/Alerts/AllerteViewController.m index 96c9cab..d083894 100644 --- a/Sources/Earthquake Network/Controllers/Alerts/AllerteViewController.m +++ b/Sources/Earthquake Network/Controllers/Alerts/AllerteViewController.m @@ -120,8 +120,9 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) { 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) { + EQNRealtimeAlert *alert = [[EQNRealtimeAlert alloc] initWithNotification:info]; + if (alert) { + RealtimeAlertViewController *controller = [[RealtimeAlertViewController alloc] initWithAlert:alert]; if (@available(iOS 13.0, *)) { controller.modalInPresentation = YES; } diff --git a/Sources/Earthquake Network/Controllers/Realtime Alert/RealtimeAlertViewController.swift b/Sources/Earthquake Network/Controllers/Realtime Alert/RealtimeAlertViewController.swift index 3aba8f5..fb8eb65 100644 --- a/Sources/Earthquake Network/Controllers/Realtime Alert/RealtimeAlertViewController.swift +++ b/Sources/Earthquake Network/Controllers/Realtime Alert/RealtimeAlertViewController.swift @@ -16,17 +16,9 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate { // 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 + /// Alert to display + private let realtimeAlert: EQNRealtimeAlert /// Timer to constantly update countdown label private var countdownTimer: Timer? /// Refresh time for wave animation @@ -37,30 +29,15 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate { /// 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 - + init(alert: EQNRealtimeAlert) { + self.realtimeAlert = alert super.init(nibName: nil, bundle: nil) self.waveAnimationCurrentRadius = currentWavePosition() - self.waveAnimationVelocity = evaluateWaveVelocity() + self.waveAnimationVelocity = evaluateWaveAnimationVelocity() } required init?(coder: NSCoder) { @@ -90,39 +67,35 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate { } 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) - } + notificationView.descriptionLabel.text = realtimeAlert.title // update title with distance from earthquake - let distance = EQNUser.default().lastPosition?.distance(from: coordinate) ?? 0.0 - let distanceRound = Int(round(distance / 1_000)) + let distanceRound = Int(round(realtimeAlert.distanceFromUser() / 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) + let region = MKCoordinateRegion(center: realtimeAlert.coordinate.coordinate, span: span) + notificationView.mapView.setCenter(realtimeAlert.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) + notificationView.addMapAnnotation(center: realtimeAlert.coordinate.coordinate, intensity: realtimeAlert.intensity) // simuliamo animazione dell'onda sismica - notificationView.addMapCircle(center: coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation") + notificationView.addMapCircle(center: realtimeAlert.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]) + notificationView.addMapLine(coordinates: [realtimeAlert.coordinate.coordinate, lastPosition.coordinate]) } } private func startCountdown() { // show countdown only if time is less than 300 seconds - if currentCountdown < 300 { + if realtimeAlert.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) @@ -146,9 +119,10 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate { // MARK: - Timer @objc private func countdownTimerFired(_ sender: Timer) { - notificationView.waveTimeLabel.text = String(format: NSLocalizedString("alert_wave", comment: ""), currentCountdown) + let countdown = realtimeAlert.currentCountdown() + notificationView.waveTimeLabel.text = String(format: NSLocalizedString("alert_wave", comment: ""), countdown) - if currentCountdown <= 0 { + if countdown <= 0 { // stop the countdown countdownTimer?.invalidate() countdownTimer = nil @@ -157,57 +131,27 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate { @objc private func mapWaveAnimationFired(_ sender: Timer) { waveAnimationCurrentRadius += waveAnimationVelocity - notificationView.addMapCircle(center: coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation") + notificationView.addMapCircle(center: realtimeAlert.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 + let distance = realtimeAlert.distanceFromUser() // calcoliamo la distanza rimanente da mostrare, perchè la schermata potrebbe anche essere aperta in ritardo - let remainingDistance = waveSpeed * Double(currentCountdown) + let remainingDistance = realtimeAlert.waveSpeed * Double(realtimeAlert.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 - + private func evaluateWaveAnimationVelocity() -> Double { + let velocity = realtimeAlert.waveSpeed return velocity * waveAnimationRefreshRate } } diff --git a/Sources/Earthquake Network/Models/EQNRealtimeAlert.swift b/Sources/Earthquake Network/Models/EQNRealtimeAlert.swift new file mode 100644 index 0000000..696e9da --- /dev/null +++ b/Sources/Earthquake Network/Models/EQNRealtimeAlert.swift @@ -0,0 +1,91 @@ +// +// EQNRealtimeAlert.swift +// Earthquake Network +// +// Created by Andrea Busi on 17/06/22. +// Copyright © 2022 Earthquake Network. All rights reserved. +// + +import Foundation +import CoreLocation + + +@objc +class EQNRealtimeAlert: NSObject { + + typealias NotificationPayload = [String: Any] + + /// Title to display + var title: String = "" + /// Earthquake coordinate + let coordinate: CLLocation + /// Earthquake intensity + let intensity: Int + /// Earthquake wave speed + let waveSpeed: Double + /// Calculated timestamp for earthquake on user position + let impactTimestamp: Date + + // MARK: - Init + + @objc + init?(notification: [String: Any]) { + guard let alert = Self.getPushAlertPayload(from: notification), + let coordinate = Self.getCoordinate(from: notification), + let impactTimestamp = EQNUtility.calculateUserSeismicTimestamp(fromUserInfo: notification) else { + return nil + } + + self.coordinate = coordinate + self.impactTimestamp = impactTimestamp + if let title = alert["loc-key"] as? String, let args = alert["loc-args"] as? [String], let arg = args.first { + self.title = String(format: NSLocalizedString(title, comment: ""), arg) + } + self.intensity = notification.eqn_intValue(for: "intensity") ?? 0 + self.waveSpeed = (notification.eqn_doubleValue(for: "wave_speed") ?? 0) * 1000 // m/s + } + + func distanceFromUser() -> CLLocationDistance { + EQNUser.default().lastPosition?.distance(from: coordinate) ?? 0.0 + } + + /// Remaining time before earthquake gets user position + func currentCountdown() -> Int { + let now = Date() + let difference = lround(max(impactTimestamp.timeIntervalSince(now), 0)) + return difference + } + + @objc + func isCountdownExpired() -> Bool { + currentCountdown() <= 0 + } + + // MARK: - Helpers + + /// 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 + } + + /// 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) + } +}