Compare commits

..

53 Commits

Author SHA1 Message Date
Andrea Busi f85c60fdda release: Increase version for release 2025-07-24 22:07:38 +02:00
Andrea Busi 5f2a083789 fix: Update Twitter to X 2025-07-24 22:07:19 +02:00
Andrea Busi 9e54f74847 release: Increase version for release 2025-07-24 20:44:44 +02:00
Andrea Busi b6f9232f56 fix: Set color based on shake intensity 2025-07-24 20:44:11 +02:00
Andrea Busi dee14dea0f release: Increase version for release 2025-07-24 20:43:57 +02:00
Andrea Busi db0bde2f59 refactor: Remove no longer needed checks for iOS < 15 2025-07-24 16:46:29 +02:00
Andrea Busi 79d0d27ec5 feat: Rewrite NotificationContent in Swift and add wave animation
Resolves: https://github.com/futurainnovation/eqn.ios/issues/4
2025-07-24 16:44:55 +02:00
Andrea Busi 68012ec406 refactor: Improve notification content 2025-07-24 11:56:49 +02:00
Andrea Busi 59feb7699b release: Increase version for release 2025-07-18 17:08:19 +02:00
Andrea Busi 388f4e8b89 feat: Add new felt value as provider parameter
Resolves: https://github.com/futurainnovation/eqn.ios/issues/1
2025-07-18 17:04:29 +02:00
Andrea Busi ca3c9ebd83 refactor: Always use updateFilter method when updating filter values 2025-07-18 17:03:34 +02:00
Andrea Busi f23c19bce7 feat: Improve UI in notification content
Resolves: https://github.com/futurainnovation/eqn.ios/issues/4
2025-07-18 17:02:52 +02:00
Andrea Busi 276fa2032a release: Increase version for release 2025-07-18 12:44:30 +02:00
Andrea Busi 09f0d4d4d8 dependency: Bump Firebase 2025-07-18 12:44:22 +02:00
Andrea Busi 25f061ad5a release: Update changelog 2025-07-18 12:40:59 +02:00
Andrea Busi b9d9f7579c feat: Add filters recap view in seismic networks
Resolves: https://github.com/futurainnovation/eqn.ios/issues/2
2025-07-18 12:40:59 +02:00
Andrea Busi 39f5ff0249 fix: Use extensions from Shogun 2025-07-18 12:28:22 +02:00
Andrea Busi af68d70be5 feat: Increase minimum target to iOS 15 2025-07-18 12:28:22 +02:00
Andrea Busi dab999a78d dependency: Bump Shogun to v2.0 2025-07-18 12:28:22 +02:00
Andrea Busi f5ede5c26d refactor: Use Roman numerals in shakemap labels
Resolves: https://github.com/futurainnovation/eqn.ios/issues/3
2025-07-17 15:52:09 +02:00
Andrea Busi 6d4c1eb979 fix: Check array bounds to avoid crash 2025-05-15 10:31:41 +02:00
Andrea Busi 9bf6b75dac release: Increase version for release 2025-05-02 10:31:57 +02:00
Andrea Busi 69b83e9944 feat: Add settings to disable alert sound for mild quakes
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/89
2025-05-02 10:31:57 +02:00
Andrea Busi 5061e36a45 refactor: Reorganize some code 2025-05-02 10:31:57 +02:00
Andrea Busi 8919f3c08f refactor: Remove some unused @objc 2025-05-02 10:31:57 +02:00
Andrea Busi 9cf9ef8a64 feat: Use cache endpoint and round values to use server cache
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/90
2025-05-02 10:31:57 +02:00
Andrea Busi 9ee3a478f0 dependency: Update Firebase 2025-05-02 10:31:57 +02:00
Andrea Busi 8744595b56 release: Increase version for release 2025-04-25 22:32:54 +02:00
Andrea Busi fa05d6b5c4 fix: Rework init to avoid crash when server returns null values 2025-04-25 22:31:44 +02:00
Andrea Busi 471ccc9e4a release: Increase version for release 2025-04-24 12:49:25 +02:00
Andrea Busi 55de6f5ba0 refactor: Use project to set version and build number 2025-04-24 12:49:08 +02:00
Andrea Busi b12a9cc476 fix: Solve wrong sort in subscriptions 2025-04-24 12:46:53 +02:00
Andrea Busi a2f740b0a8 release: Increase version for release 2025-03-07 17:57:54 +01:00
Andrea Busi 9cf93e652b fix: Reset cell informations 2025-03-07 15:38:51 +01:00
Andrea Busi 2d23056ba8 release: Increase version for release 2025-03-07 14:10:47 +01:00
Andrea Busi cb6ecca774 refactor: Hide description label when taking screenshot in intensity map 2025-03-07 14:10:47 +01:00
Andrea Busi 96286a49f6 feat: Upgrade Xcode project version 2025-03-07 14:00:28 +01:00
Andrea Busi 481e8a28c0 fix: Solve crash with a single shakemape curve 2025-03-07 14:00:16 +01:00
Andrea Busi 286a4062f5 release: Increase version for release 2025-03-07 11:16:35 +01:00
Andrea Busi 01a8ad7419 fix: Solve wrong calculation in scroll indicator 2025-03-07 11:16:07 +01:00
Andrea Busi 6e97e9bd2c release: Increase version for release 2025-03-07 09:56:03 +01:00
Andrea Busi af6e94efcb fix: Solve non working intensity map in minimal cell 2025-03-07 09:45:45 +01:00
Andrea Busi 5387758449 fix: Select yearly as default subscription 2025-03-07 09:42:26 +01:00
Andrea Busi 054603b42d release: Increase version for release 2025-03-06 17:57:41 +01:00
Andrea Busi caf0e3b7cc feat: Add new minimal card in seismics list
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/87
2025-03-06 17:57:07 +01:00
Andrea Busi 4c35c38cc5 refactor: Move card informations to AppPreferences 2025-03-06 15:49:26 +01:00
Andrea Busi 521254c8c1 refactor: Remove unused constant 2025-03-06 13:01:20 +01:00
Andrea Busi 78a1710584 refactor: Replace deprecated methods 2025-03-06 10:59:36 +01:00
Andrea Busi b2a54a544c feat: Update layout in Seismic List section
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/86
2025-03-06 10:45:02 +01:00
Andrea Busi 0f5ad24744 feat: Update layout in Subscriptions section
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/85
2025-03-06 10:19:14 +01:00
Andrea Busi 0296cd50cd feat: Update layout in Reports section
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/84
2025-03-06 10:18:44 +01:00
Andrea Busi 7551988b4e feat: Update layout in Alerts section
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/83
2025-03-06 10:17:42 +01:00
Andrea Busi 5edcaaad99 feat: Change layout for base container card (center title) 2025-03-06 10:17:08 +01:00
60 changed files with 1714 additions and 847 deletions
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## 5.11
- Numeri romani in etichette shakemaps
- Aggiunta barra di recap dei filtri in Lista Sismi
- Riscritta NotificationContent in Swift
- Aggiunta animazione onda sismica in NotificationContent (+ altre modifiche grafiche)
## 5.10
- Usato endpoint cache per distquake_download_areacheck
- Aggiunta impostazione per non riprodurre suono notifiche per simsi deboli
## Versione 5.9.1
- Corretto ordinamento in sottoscrizioni attive (prima Top10k)
- Modificato parsing per risolvere crash con valori nulli
## Versione 5.9
- Aggiunta barra laterale in lista sismi
- Aggiunto filtro "sismi percepiti"
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="M4Y-Lb-cyx">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="M4Y-Lb-cyx">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -11,37 +11,45 @@
<!--Notification View Controller-->
<scene sceneID="cwh-vc-ff4">
<objects>
<viewController id="M4Y-Lb-cyx" userLabel="Notification View Controller" customClass="NotificationViewController" sceneMemberID="viewController">
<viewController id="M4Y-Lb-cyx" userLabel="Notification View Controller" customClass="NotificationContentViewController" customModule="EQNNotificationContent" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" simulatedAppContext="notificationCenter" id="S3S-Oj-5AN">
<rect key="frame" x="0.0" y="0.0" width="320" height="330"/>
<rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pCT-Wh-lut">
<rect key="frame" x="8" y="216" width="304" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pCT-Wh-lut">
<rect key="frame" x="8" y="350" width="304" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bT3-3m-qLh">
<rect key="frame" x="8" y="285" width="304" height="29"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bT3-3m-qLh">
<rect key="frame" x="8" y="417" width="304" height="53"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/>
<color key="textColor" red="0.91764705879999997" green="0.46274509800000002" blue="0.0078431372550000003" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<mapView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" mapType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="4ID-Zb-OQF">
<rect key="frame" x="0.0" y="0.0" width="320" height="200"/>
<mapView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" mapType="standard" showsUserLocation="YES" translatesAutoresizingMaskIntoConstraints="NO" id="4ID-Zb-OQF">
<rect key="frame" x="0.0" y="20" width="320" height="320"/>
<constraints>
<constraint firstAttribute="height" constant="200" id="Pgl-8e-ePq"/>
<constraint firstAttribute="width" secondItem="4ID-Zb-OQF" secondAttribute="height" multiplier="1:1" id="yXb-UG-FZY"/>
</constraints>
<connections>
<outlet property="delegate" destination="M4Y-Lb-cyx" id="Cs2-OY-eT2"/>
</connections>
</mapView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f3d-th-bgU">
<rect key="frame" x="8" y="244.5" width="304" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f3d-th-bgU">
<rect key="frame" x="8" y="380.5" width="304" height="26.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="s7c-ag-zBA" customClass="EQNBlurredCloseButton" customModule="EQNNotificationContent" customModuleProvider="target">
<rect key="frame" x="103" y="26" width="114" height="35"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="Tap to open"/>
<connections>
<action selector="openAppTapped:" destination="M4Y-Lb-cyx" eventType="touchUpInside" id="Sw3-xS-cXi"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="2BE-c3-nQJ"/>
<constraints>
@@ -49,23 +57,26 @@
<constraint firstItem="f3d-th-bgU" firstAttribute="leading" secondItem="pCT-Wh-lut" secondAttribute="leading" id="7qA-vV-ocI"/>
<constraint firstItem="2BE-c3-nQJ" firstAttribute="trailing" secondItem="pCT-Wh-lut" secondAttribute="trailing" constant="8" id="CAC-UM-SaJ"/>
<constraint firstItem="f3d-th-bgU" firstAttribute="trailing" secondItem="pCT-Wh-lut" secondAttribute="trailing" id="Dd7-BF-iOG"/>
<constraint firstItem="f3d-th-bgU" firstAttribute="top" secondItem="pCT-Wh-lut" secondAttribute="bottom" constant="8" id="FJ8-nn-ydU"/>
<constraint firstItem="2BE-c3-nQJ" firstAttribute="bottom" secondItem="bT3-3m-qLh" secondAttribute="bottom" constant="16" id="I71-6U-jK3"/>
<constraint firstItem="pCT-Wh-lut" firstAttribute="top" secondItem="4ID-Zb-OQF" secondAttribute="bottom" constant="16" id="It9-RA-906"/>
<constraint firstItem="f3d-th-bgU" firstAttribute="top" secondItem="pCT-Wh-lut" secondAttribute="bottom" constant="10" id="FJ8-nn-ydU"/>
<constraint firstItem="s7c-ag-zBA" firstAttribute="top" secondItem="2BE-c3-nQJ" secondAttribute="top" constant="6" id="Gsp-ye-V4M"/>
<constraint firstItem="2BE-c3-nQJ" firstAttribute="bottom" secondItem="bT3-3m-qLh" secondAttribute="bottom" constant="10" id="I71-6U-jK3"/>
<constraint firstItem="pCT-Wh-lut" firstAttribute="top" secondItem="4ID-Zb-OQF" secondAttribute="bottom" constant="10" id="It9-RA-906"/>
<constraint firstItem="bT3-3m-qLh" firstAttribute="trailing" secondItem="f3d-th-bgU" secondAttribute="trailing" id="KXf-x4-iZs"/>
<constraint firstItem="bT3-3m-qLh" firstAttribute="leading" secondItem="f3d-th-bgU" secondAttribute="leading" id="QlJ-Vh-oi4"/>
<constraint firstItem="bT3-3m-qLh" firstAttribute="top" secondItem="f3d-th-bgU" secondAttribute="bottom" constant="20" id="UUO-2F-eE7"/>
<constraint firstItem="bT3-3m-qLh" firstAttribute="top" secondItem="f3d-th-bgU" secondAttribute="bottom" constant="10" id="UUO-2F-eE7"/>
<constraint firstItem="4ID-Zb-OQF" firstAttribute="trailing" secondItem="2BE-c3-nQJ" secondAttribute="trailing" id="buf-BU-I5b"/>
<constraint firstItem="4ID-Zb-OQF" firstAttribute="leading" secondItem="2BE-c3-nQJ" secondAttribute="leading" id="e8D-ji-t64"/>
<constraint firstItem="4ID-Zb-OQF" firstAttribute="top" secondItem="2BE-c3-nQJ" secondAttribute="top" id="hL6-gc-S6i"/>
<constraint firstItem="s7c-ag-zBA" firstAttribute="centerX" secondItem="S3S-Oj-5AN" secondAttribute="centerX" id="mKj-2O-QDw"/>
</constraints>
</view>
<extendedEdge key="edgesForExtendedLayout"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<size key="freeformSize" width="320" height="330"/>
<size key="freeformSize" width="320" height="480"/>
<connections>
<outlet property="descriptionLabel" destination="f3d-th-bgU" id="Aym-KJ-DqY"/>
<outlet property="mapView" destination="4ID-Zb-OQF" id="x8o-nT-bL4"/>
<outlet property="tapToOpenButton" destination="s7c-ag-zBA" id="4aZ-hT-1vc"/>
<outlet property="titleLabel" destination="pCT-Wh-lut" id="uIg-dn-Wms"/>
<outlet property="waveLabel" destination="bT3-3m-qLh" id="AkJ-nd-d2R"/>
</connections>
@@ -0,0 +1,144 @@
//
// NotificationContentViewController.swift
// EQNNotificationContent
//
// Created by Andrea Busi on 24/07/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import UIKit
import MapKit
import UserNotifications
import UserNotificationsUI
class NotificationContentViewController: UIViewController {
// MARK: - UI
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var descriptionLabel: UILabel!
@IBOutlet private weak var waveLabel: UILabel!
@IBOutlet private weak var mapView: MKMapView!
@IBOutlet private weak var tapToOpenButton: UIButton!
private var animator: MapSeismicWaveAnimator?
// MARK: - View
override func viewDidLoad() {
super.viewDidLoad()
let tapRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(openAppTapped(_:))
)
view.addGestureRecognizer(tapRecognizer)
mapView
.register(
EQNCustomAnnotationView.self,
forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier
)
mapView
.register(
EQNCustomAnnotationView.self,
forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SmallIdentifier
)
tapToOpenButton.setTitle("tap_to_open".localized, for: .normal)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
animator?.stop()
}
// MARK: - Actions
@IBAction private func openAppTapped(_ sender: Any) {
extensionContext?.performNotificationDefaultAction()
}
}
extension NotificationContentViewController: UNNotificationContentExtension {
func didReceive(_ notification: UNNotification) {
let content = notification.request.content
titleLabel.text = content.title
descriptionLabel.text = content.body
let notification = EQNRealtimePushNotification.from(
userInfo: content.userInfo,
title: "",
displayTitle: content.title,
displayBody: content.body
)
if let notification {
let coordinate = notification.coordinate.coordinate
let span = MKCoordinateSpan(latitudeDelta: 6, longitudeDelta: 6)
let region = MKCoordinateRegion(
center: coordinate,
span: span
)
mapView.setCenter(coordinate, animated: false)
mapView.setRegion(region, animated: true)
switch notification.type.lowercased() {
case "eqn":
let annotation = EQNMapAnnotationPastquake(
title: "",
coordinate: coordinate,
intensity: notification.intensity
)
mapView.addAnnotation(annotation)
case "manual":
let annotation = EQNMapAnnotationUserReport(
magnitude: notification.magnitude,
coordinate: coordinate
)
mapView.addAnnotation(annotation)
default:
break
}
// create animator to manage wave animation and countdown
animator = MapSeismicWaveAnimator(
realtimeAlert: notification,
mapView: mapView,
waveTimeLabel: waveLabel
)
animator?.start()
// set color based on intensity
descriptionLabel.textColor = notification.relativeIntensityColor
}
}
}
extension NotificationContentViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: any MKAnnotation) -> MKAnnotationView? {
switch annotation {
case let pastquake as EQNMapAnnotationPastquake:
let annotationView = mapView.dequeueReusableAnnotationView(
withIdentifier: EQNCustomAnnotationView.SingleLineIdentifier
) as! EQNCustomAnnotationView
annotationView.image = pastquake.image
annotationView.title = pastquake.title
return annotationView
case let report as EQNMapAnnotationUserReport:
let annotationView = mapView.dequeueReusableAnnotationView(
withIdentifier: EQNCustomAnnotationView.SmallIdentifier
) as! EQNCustomAnnotationView
annotationView.image = report.image(with: EQNCustomAnnotationView.SmallViewImageHeight)
annotationView.title = report.timeDifference
return annotationView
default:
return nil
}
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
animator?.getOverlayRenderer(for: overlay) ?? MKOverlayRenderer(overlay: overlay)
}
}
@@ -1,13 +0,0 @@
//
// NotificationViewController.h
// EQNNotificationContent
//
// Refactored by Andrea Busi on 14/10/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface NotificationViewController : UIViewController
@end
@@ -1,129 +0,0 @@
//
// NotificationViewController.m
// EQNNotificationContent
//
// Refactored by Andrea Busi on 14/10/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "NotificationViewController.h"
#import "EQNUtility.h"
#import "EQNNotificationContent-Swift.h"
@import UserNotifications;
@import UserNotificationsUI;
@import MapKit;
@interface NotificationViewController () <UNNotificationContentExtension, MKMapViewDelegate>
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UILabel *descriptionLabel;
@property (weak, nonatomic) IBOutlet UILabel *waveLabel;
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
/// This will be calculated as seismic date + warning time
@property (strong, nonatomic) NSDate *userSeismicTimestamp;
@property (strong, nonatomic) NSTimer *countdownTimer;
@end
@implementation NotificationViewController
- (void)setMapView:(MKMapView *)mapView
{
_mapView = mapView;
_mapView.scrollEnabled = NO;
_mapView.zoomEnabled = NO;
}
#pragma mark - View Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self.mapView registerClass:[EQNCustomAnnotationView class] forAnnotationViewWithReuseIdentifier:EQNCustomAnnotationView.SingleLineIdentifier];
[self.mapView registerClass:[EQNCustomAnnotationView class] forAnnotationViewWithReuseIdentifier:EQNCustomAnnotationView.SmallIdentifier];
}
- (void)didReceiveNotification:(UNNotification *)notification
{
UNNotificationContent *content = notification.request.content;
NSDictionary *userInfo = content.userInfo;
// set title and description
self.titleLabel.text = content.title;
self.descriptionLabel.text = content.body;
// add annotation onthe map
CLLocation *coordinate = [[CLLocation alloc] initWithLatitude:[userInfo[@"latitude"] doubleValue]
longitude:[userInfo[@"longitude"] doubleValue]];
MKCoordinateSpan span = MKCoordinateSpanMake(10.5, 10.5);
MKCoordinateRegion region = MKCoordinateRegionMake(coordinate.coordinate, span);
[self.mapView setCenterCoordinate:coordinate.coordinate animated:NO];
[self.mapView setRegion:region animated:YES];
if ([userInfo[@"type"] isEqualToString:@"eqn"]) {
EQNMapAnnotationPastquake *annotation = [[EQNMapAnnotationPastquake alloc] initWithTitle:@""
coordinate:coordinate.coordinate
intensity:[userInfo[@"intensity"] intValue]];
[self.mapView addAnnotation:annotation];
} else if ([userInfo[@"type"] isEqualToString:@"manual"]){
EQNMapAnnotationUserReport *annotation = [[EQNMapAnnotationUserReport alloc] initWithMagnitude:[userInfo[@"magnitude"] intValue]
coordinate:coordinate.coordinate];
[self.mapView addAnnotation:annotation];
}
self.userSeismicTimestamp = [EQNUtility calculateUserSeismicTimestampFromUserInfo:userInfo];
if (self.userSeismicTimestamp) {
// start the countdown
self.countdownTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(countdownFired:)
userInfo:nil
repeats:YES];
[self.countdownTimer fire];
}
}
#pragma mark - Private
- (void)countdownFired:(id)sender
{
NSDate *now = [NSDate date];
NSTimeInterval difference = MAX([self.userSeismicTimestamp timeIntervalSinceDate:now], 0);
NSInteger seconds = (int)lround(difference);
self.waveLabel.text = [NSString localizedStringWithFormat:NSLocalizedString(@"alert_wave", @""), seconds];
if (difference <= 0) {
// stop the countdown
[self.countdownTimer invalidate];
self.countdownTimer = nil;
}
}
#pragma mark - MKMapViewDelegate
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[EQNMapAnnotationPastquake class]]) {
EQNMapAnnotationPastquake *pastquake = (EQNMapAnnotationPastquake *)annotation;
EQNCustomAnnotationView *annotationView = (EQNCustomAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:EQNCustomAnnotationView.SingleLineIdentifier];
annotationView.image = pastquake.image;
annotationView.title = pastquake.title;
return annotationView;
} else if ([annotation isKindOfClass:[EQNMapAnnotationUserReport class]]) {
EQNMapAnnotationUserReport *report = (EQNMapAnnotationUserReport *)annotation;
EQNCustomAnnotationView *annotationView = (EQNCustomAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:EQNCustomAnnotationView.SmallIdentifier];
annotationView.image = [report imageWithHeight:EQNCustomAnnotationView.SmallViewImageHeight];
annotationView.title = report.timeDifference;
return annotationView;
}
return nil;
}
@end
@@ -51,6 +51,43 @@ class NotificationService: UNNotificationServiceExtension {
bestAttemptContent.sound = UNNotificationSound(named: Self.EQNSoundNotification)
}
// evaluate intensity and get proper string to display
guard let latitude = userInfo.double(forKey: "latitude"),
let longitude = userInfo.double(forKey: "longitude"),
let peak = userInfo.double(forKey: "peak") else {
print("[NotificationService] Unable to get base info for intensity calculation")
return
}
let magnitude = userInfo.double(forKey: "mag") ?? 0
let location = CLLocation(latitude: latitude, longitude: longitude)
guard let distance = EQNUserData.shared.lastLocation?.distance(from: location) else {
print("[NotificationService] Unable to calculate distance or get last location")
return
}
let distanceKm = distance / 1_000
// If the shake is mild, user can disale sound and use default notification sound
// This logic is done here and not with the rest because distance and other informations are needed
let mildQuakeSoundDisabled = UserDefaults.appGroup?.bool(forKey: UserDefaults.AllertaSismicaSuonoDisabilitatoSismaDebole) ?? true
let isMildQuake = isMildQuake(magnitude: magnitude, distance: distanceKm)
if isMildQuake && mildQuakeSoundDisabled {
bestAttemptContent.sound = UNNotificationSound.default
}
let intensita = peak * exp(-distanceKm/peak/250)
let stringSuffix = if intensita < 0.004 {
"no_shaking"
} else if intensita < 0.30 {
"mild"
} else if intensita < 0.70 {
"moderate"
} else {
"strong"
}
bestAttemptContent.body = "alert_intensity_\(stringSuffix)".localized
let intensity = userInfo.integer(forKey: "intensity")
switch intensity {
case 0:
@@ -63,35 +100,6 @@ class NotificationService: UNNotificationServiceExtension {
break
}
// evaluate intensity and get proper string to display
guard let latitude = userInfo.double(forKey: "latitude"),
let longitude = userInfo.double(forKey: "longitude"),
let peak = userInfo.double(forKey: "peak") else {
print("[NotificationService] Unable to get base info for intensity calculation")
return
}
let location = CLLocation(latitude: latitude, longitude: longitude)
guard let distance = EQNUserData.shared.lastLocation?.distance(from: location) else {
print("[NotificationService] Unable to calculate distance or get last location")
return
}
let distanceKm = distance / 1_000
let intensita = peak * exp(-distanceKm/peak/250)
let stringSuffix: String
if intensita < 0.004 {
stringSuffix = "no_shaking"
} else if intensita < 0.30 {
stringSuffix = "mild"
} else if intensita < 0.70 {
stringSuffix = "moderate"
} else {
stringSuffix = "strong"
}
bestAttemptContent.body = "alert_intensity_\(stringSuffix)".localized
case "manual":
// there are 12 levels, so a customized icon doesn't make sense
// use a generic warning icon instead
@@ -159,7 +167,7 @@ class NotificationService: UNNotificationServiceExtension {
notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers)
// !! Note: this is a known issue/bug
// we need to add a delay before invoking the completion, otherwise the notification will not be remved
// we need to add a delay before invoking the completion, otherwise the notification will not be removed
// ref: https://stackoverflow.com/questions/53697279/why-are-notifications-not-removed-with-removedeliverednotifications
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
completion()
@@ -167,6 +175,20 @@ class NotificationService: UNNotificationServiceExtension {
}
}
private func isMildQuake(
magnitude: Double,
distance: Double
) -> Bool {
var intensity_at_location: Double = 0
if distance > 0 {
let R: Double = 6371
let eq_depth: Double = 10.0
let hyp_distance = sqrt(pow(eq_depth, 2) + 4 * R * (R - eq_depth) * pow(sin(distance / (2 * R)), 2))
intensity_at_location = -2.15 * log10(hyp_distance) + 1.0 * magnitude + 2.31
}
return intensity_at_location < 3
}
// MARK: - Helpers
private func manualIconName(for provider: String, color: String) -> String {
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
@@ -18,10 +18,13 @@
6514FF6D2D720CBE000A7BD0 /* MapPinStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6514FF692D720C3A000A7BD0 /* MapPinStyle.swift */; };
65172F532C25C496006D2A5C /* EQNSeismicAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65172F522C25C496006D2A5C /* EQNSeismicAnnotationView.swift */; };
651901B925F5358700CAFF20 /* EQNMapAnnotationSeismic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651901B825F5358700CAFF20 /* EQNMapAnnotationSeismic.swift */; };
652247242D79DAA000D2B8DF /* SeismicNetworkMinimalTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 652247232D79DAA000D2B8DF /* SeismicNetworkMinimalTableViewCell.swift */; };
652247262D79EC5E00D2B8DF /* SeismicNetworkBaseTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 652247252D79EC5E00D2B8DF /* SeismicNetworkBaseTableViewCell.swift */; };
6525A82625E13FD4008FE0D0 /* SeismicNetworkAdvertiseTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6525A82525E13FD4008FE0D0 /* SeismicNetworkAdvertiseTableViewCell.swift */; };
652A3C6B2A8A757800719796 /* EQNBackgrounPositionDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 652A3C6A2A8A757800719796 /* EQNBackgrounPositionDebugViewController.swift */; };
65355FFF25F38D3300BB57D2 /* SegnalazioniMapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65355FFE25F38D3300BB57D2 /* SegnalazioniMapViewController.swift */; };
653604E9262348FA00B2B651 /* EQNBaseMapFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653604E8262348FA00B2B651 /* EQNBaseMapFilter.swift */; };
653C5C2C2E295C5A00E25528 /* SeismicNetworkFilterRecapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653C5C2B2E295C5A00E25528 /* SeismicNetworkFilterRecapView.swift */; };
653C67E225F3CC2E00FE52AC /* EQNCustomAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653C67E125F3CC2E00FE52AC /* EQNCustomAnnotationView.swift */; };
653C67E625F3CC8400FE52AC /* EQNCustomAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653C67E125F3CC2E00FE52AC /* EQNCustomAnnotationView.swift */; };
653C67FC25F3D63500FE52AC /* EQNMapAnnotationUserReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653C67FB25F3D63500FE52AC /* EQNMapAnnotationUserReport.swift */; };
@@ -39,6 +42,7 @@
65583A05261B83BE00ECA9F9 /* UIKit+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65583A04261B83BE00ECA9F9 /* UIKit+Extensions.swift */; };
6562C80725FFA6B100C85273 /* SeismicNetworkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6562C80625FFA6B100C85273 /* SeismicNetworkViewModel.swift */; };
6563DAA42AAF515F0072D309 /* BackgroundTaskIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6563DAA32AAF515F0072D309 /* BackgroundTaskIdentifiable.swift */; };
6568C2D42E2A930500402F16 /* EQNBlurredCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6586971025F44C26009C0182 /* EQNBlurredCloseButton.swift */; };
656D138F2C2225560094F597 /* SubscriptionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656D138E2C2225560094F597 /* SubscriptionDetailsViewController.swift */; };
656D13912C22371F0094F597 /* SubscriptionDetailsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656D13902C22371F0094F597 /* SubscriptionDetailsTableViewCell.swift */; };
656E02162C1C4DF2008D0E92 /* EQNBaseContainerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656E02152C1C4DF2008D0E92 /* EQNBaseContainerTableViewCell.swift */; };
@@ -47,6 +51,11 @@
657415E02D70B6F800F54890 /* EQNMapAnnotationShakemap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657415DE2D70B6F700F54890 /* EQNMapAnnotationShakemap.swift */; };
657747FD2D4D12A200213830 /* SeismicNetworkData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657747FC2D4D12A200213830 /* SeismicNetworkData.swift */; };
657747FF2D4D2BA200213830 /* SeismicNetworkScrollIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657747FE2D4D2BA100213830 /* SeismicNetworkScrollIndicatorView.swift */; };
657CCF082E323AF700E91715 /* NotificationContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657CCF072E323AF700E91715 /* NotificationContentViewController.swift */; };
657CCF092E323BE500E91715 /* EQNRealtimePushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65EA58812A60360D0038EE9D /* EQNRealtimePushNotification.swift */; };
657CCF0A2E323DC600E91715 /* Foundation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 650247142A618D7F001AC512 /* Foundation+Extensions.swift */; };
657CCF0C2E32711D00E91715 /* MapSeismicWaveAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657CCF0B2E32711D00E91715 /* MapSeismicWaveAnimator.swift */; };
657CCF0D2E32711D00E91715 /* MapSeismicWaveAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657CCF0B2E32711D00E91715 /* MapSeismicWaveAnimator.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 */; };
@@ -222,7 +231,6 @@
8CF05B57218C93BA0055012B /* EQNUtility.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CF05B56218C93BA0055012B /* EQNUtility.m */; };
8CF12CD321DE49B600613AC5 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8CF12CD221DE49B600613AC5 /* UserNotifications.framework */; };
8CF12CD521DE49B600613AC5 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8CF12CD421DE49B600613AC5 /* UserNotificationsUI.framework */; };
8CF12CD921DE49B600613AC5 /* NotificationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CF12CD821DE49B600613AC5 /* NotificationViewController.m */; };
8CF12CDC21DE49B600613AC5 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8CF12CDA21DE49B600613AC5 /* MainInterface.storyboard */; };
8CF12CE021DE49B600613AC5 /* EQNNotificationContent.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8CF12CD121DE49B600613AC5 /* EQNNotificationContent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
8CF4F4D8216D3A110057110B /* EQNAreaCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CF4F4D7216D3A110057110B /* EQNAreaCheck.m */; };
@@ -320,11 +328,14 @@
6514FF692D720C3A000A7BD0 /* MapPinStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapPinStyle.swift; sourceTree = "<group>"; };
65172F522C25C496006D2A5C /* EQNSeismicAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNSeismicAnnotationView.swift; sourceTree = "<group>"; };
651901B825F5358700CAFF20 /* EQNMapAnnotationSeismic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNMapAnnotationSeismic.swift; sourceTree = "<group>"; };
652247232D79DAA000D2B8DF /* SeismicNetworkMinimalTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeismicNetworkMinimalTableViewCell.swift; sourceTree = "<group>"; };
652247252D79EC5E00D2B8DF /* SeismicNetworkBaseTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeismicNetworkBaseTableViewCell.swift; sourceTree = "<group>"; };
6525A82525E13FD4008FE0D0 /* SeismicNetworkAdvertiseTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeismicNetworkAdvertiseTableViewCell.swift; sourceTree = "<group>"; };
652A3C6A2A8A757800719796 /* EQNBackgrounPositionDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNBackgrounPositionDebugViewController.swift; sourceTree = "<group>"; };
65355FFE25F38D3300BB57D2 /* SegnalazioniMapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegnalazioniMapViewController.swift; sourceTree = "<group>"; };
653604E8262348FA00B2B651 /* EQNBaseMapFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNBaseMapFilter.swift; sourceTree = "<group>"; };
653B791F2934B6DA00E8FFFB /* EQNNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EQNNotificationService.entitlements; sourceTree = "<group>"; };
653C5C2B2E295C5A00E25528 /* SeismicNetworkFilterRecapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeismicNetworkFilterRecapView.swift; sourceTree = "<group>"; };
653C67E125F3CC2E00FE52AC /* EQNCustomAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNCustomAnnotationView.swift; sourceTree = "<group>"; };
653C67FB25F3D63500FE52AC /* EQNMapAnnotationUserReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNMapAnnotationUserReport.swift; sourceTree = "<group>"; };
653C680325F3DF8A00FE52AC /* EQNBaseMapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNBaseMapViewController.swift; sourceTree = "<group>"; };
@@ -352,6 +363,8 @@
657415DE2D70B6F700F54890 /* EQNMapAnnotationShakemap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNMapAnnotationShakemap.swift; sourceTree = "<group>"; };
657747FC2D4D12A200213830 /* SeismicNetworkData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeismicNetworkData.swift; sourceTree = "<group>"; };
657747FE2D4D2BA100213830 /* SeismicNetworkScrollIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeismicNetworkScrollIndicatorView.swift; sourceTree = "<group>"; };
657CCF072E323AF700E91715 /* NotificationContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewController.swift; sourceTree = "<group>"; };
657CCF0B2E32711D00E91715 /* MapSeismicWaveAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSeismicWaveAnimator.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>"; };
@@ -550,8 +563,6 @@
8CF12CD121DE49B600613AC5 /* EQNNotificationContent.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = EQNNotificationContent.appex; sourceTree = BUILT_PRODUCTS_DIR; };
8CF12CD221DE49B600613AC5 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
8CF12CD421DE49B600613AC5 /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; };
8CF12CD721DE49B600613AC5 /* NotificationViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationViewController.h; sourceTree = "<group>"; };
8CF12CD821DE49B600613AC5 /* NotificationViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationViewController.m; sourceTree = "<group>"; };
8CF12CDB21DE49B600613AC5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
8CF12CDD21DE49B600613AC5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8CF4F4D6216D3A110057110B /* EQNAreaCheck.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EQNAreaCheck.h; sourceTree = "<group>"; };
@@ -674,6 +685,7 @@
children = (
658BC0282859A456009EECAA /* RealtimeAlertViewController.swift */,
658BC02A2859A4D3009EECAA /* RealtimeAlertView.swift */,
657CCF0B2E32711D00E91715 /* MapSeismicWaveAnimator.swift */,
);
path = "Realtime Alert";
sourceTree = "<group>";
@@ -909,8 +921,7 @@
children = (
6557CBBC26078A1700962757 /* EQNNotificationContent.entitlements */,
8C483CB621FDACD100259FD2 /* EQNNotificationContent-Bridging-Header.h */,
8CF12CD721DE49B600613AC5 /* NotificationViewController.h */,
8CF12CD821DE49B600613AC5 /* NotificationViewController.m */,
657CCF072E323AF700E91715 /* NotificationContentViewController.swift */,
8CF12CDA21DE49B600613AC5 /* MainInterface.storyboard */,
8CF12CDD21DE49B600613AC5 /* Info.plist */,
);
@@ -937,6 +948,7 @@
6562C80625FFA6B100C85273 /* SeismicNetworkViewModel.swift */,
657747FC2D4D12A200213830 /* SeismicNetworkData.swift */,
657747FE2D4D2BA100213830 /* SeismicNetworkScrollIndicatorView.swift */,
653C5C2B2E295C5A00E25528 /* SeismicNetworkFilterRecapView.swift */,
DC2814372519C56100C1AFF7 /* SeismicNetworksViewController.swift */,
DCC76BD7251F56050005C4DC /* SeismicCardSettingsViewController.swift */,
65DBFB4A25E29DD60041CBA6 /* SeismicNetworksMapDetailViewController.swift */,
@@ -949,7 +961,9 @@
isa = PBXGroup;
children = (
65DBFB7D25E2CB020041CBA6 /* Ad templates */,
652247252D79EC5E00D2B8DF /* SeismicNetworkBaseTableViewCell.swift */,
DC28142F2519C24400C1AFF7 /* SeismicNetworkTableViewCell.swift */,
652247232D79DAA000D2B8DF /* SeismicNetworkMinimalTableViewCell.swift */,
6525A82525E13FD4008FE0D0 /* SeismicNetworkAdvertiseTableViewCell.swift */,
);
path = Cells;
@@ -1348,7 +1362,6 @@
};
};
buildConfigurationList = 8CBD3DBD2149B9AD0070C963 /* Build configuration list for PBXProject "Earthquake Network" */;
compatibilityVersion = "Xcode 8.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
@@ -1372,6 +1385,7 @@
65B16E282BDFB39B0020527E /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */,
65E6AC6C2C2DB3B60073F8FE /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 8CBD3DC32149B9AD0070C963 /* Products */;
projectDirPath = "";
projectReferences = (
@@ -1595,8 +1609,10 @@
650247122A61832F001AC512 /* EQNBackgroundPositionDebugHelper.swift in Sources */,
65CB83432915720400EE1E35 /* EQNUserData.swift in Sources */,
654D18C425F93C0600BB6DB0 /* PasquakesMapViewController.swift in Sources */,
652247242D79DAA000D2B8DF /* SeismicNetworkMinimalTableViewCell.swift in Sources */,
8C483CBC21FDACE500259FD2 /* EQNInAppProducts.swift in Sources */,
8C483CB821FDACD300259FD2 /* IAPHelper.swift in Sources */,
652247262D79EC5E00D2B8DF /* SeismicNetworkBaseTableViewCell.swift in Sources */,
6562C80725FFA6B100C85273 /* SeismicNetworkViewModel.swift in Sources */,
DCDE0BD924E58CCE00209778 /* EQNMainTabBarController.m in Sources */,
8C4E344B2152EE5B008B0D2A /* EQNGeneratoreURLServer.m in Sources */,
@@ -1648,6 +1664,7 @@
65F9B4A02A8CC58200F13260 /* UpdateUserLocationTask.swift in Sources */,
8C4E34422152B5E8008B0D2A /* EQNRilevamento.m in Sources */,
8C7A3B66225A5EA40045B266 /* NSDictionary+EQNExtensions.m in Sources */,
653C5C2C2E295C5A00E25528 /* SeismicNetworkFilterRecapView.swift in Sources */,
65AB4A992C11DFC200950DF7 /* EQNSettingSeismicNetworkNotification.swift in Sources */,
8CF66053214C12DC009F4314 /* EQNMath.m in Sources */,
DCF4A54524F8DB8300B17326 /* SettingDateTableViewCell.swift in Sources */,
@@ -1665,6 +1682,7 @@
653C67E225F3CC2E00FE52AC /* EQNCustomAnnotationView.swift in Sources */,
8CF4F4D8216D3A110057110B /* EQNAreaCheck.m in Sources */,
8C4E34452152B707008B0D2A /* EQNAccelerometroManager.m in Sources */,
657CCF0C2E32711D00E91715 /* MapSeismicWaveAnimator.swift in Sources */,
65D409942619BA34008CF356 /* SegnalazioniSendReportCell.swift in Sources */,
65F9B49C2A8CA22800F13260 /* BackgroundTaskManager.swift in Sources */,
656D13912C22371F0094F597 /* SubscriptionDetailsTableViewCell.swift in Sources */,
@@ -1696,17 +1714,21 @@
8C465D9F21F7BE0600F04673 /* Assets.xcassets in Sources */,
65D9938C2922647800F2B0EB /* AppTheme.swift in Sources */,
DC0AE1B92538204100111307 /* EQNSegnalazione.m in Sources */,
657CCF092E323BE500E91715 /* EQNRealtimePushNotification.swift in Sources */,
657CCF082E323AF700E91715 /* NotificationContentViewController.swift in Sources */,
654D18DE25F943E200BB6DB0 /* EQNMapAnnotationUserReport.swift in Sources */,
654D18D625F9420500BB6DB0 /* EQNMapAnnotationPastquake.swift in Sources */,
65D507DB2A852274005BDC57 /* EQNUserData.swift in Sources */,
657CCF0A2E323DC600E91715 /* Foundation+Extensions.swift in Sources */,
DC0AE1B52538202300111307 /* EQNUtility.m in Sources */,
65D507DC2A85227F005BDC57 /* Constants.swift in Sources */,
657CCF0D2E32711D00E91715 /* MapSeismicWaveAnimator.swift in Sources */,
653C67E625F3CC8400FE52AC /* EQNCustomAnnotationView.swift in Sources */,
654D18DA25F9424700BB6DB0 /* EQNUtility+Extensions.swift in Sources */,
DC0AE1BA2538204100111307 /* EQNPastquakes.m in Sources */,
6568C2D42E2A930500402F16 /* EQNBlurredCloseButton.swift in Sources */,
65D9938A29219DEC00F2B0EB /* UIKit+Extensions.swift in Sources */,
6514FF6C2D720CBE000A7BD0 /* MapPinStyle.swift in Sources */,
8CF12CD921DE49B600613AC5 /* NotificationViewController.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1810,7 +1832,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 146;
CURRENT_PROJECT_VERSION = "$(inherited)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WJA4MR4CPC;
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -1821,13 +1843,12 @@
INFOPLIST_FILE = EQNNotificationService/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = EQNNotificationService;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Earthquake Network. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.9.0;
MARKETING_VERSION = "$(inherited)";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.finazzi.distquake.notificationservice;
@@ -1839,7 +1860,6 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@@ -1852,20 +1872,19 @@
CODE_SIGN_IDENTITY = "Apple Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 146;
CURRENT_PROJECT_VERSION = "$(inherited)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WJA4MR4CPC;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = EQNNotificationService/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = EQNNotificationService;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Earthquake Network. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.9.0;
MARKETING_VERSION = "$(inherited)";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.finazzi.distquake.notificationservice;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1875,7 +1894,6 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
@@ -1915,6 +1933,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 159;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = WJA4MR4CPC;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -1934,7 +1953,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 5.11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -1977,6 +1997,7 @@
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 159;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = WJA4MR4CPC;
ENABLE_NS_ASSERTIONS = NO;
@@ -1990,7 +2011,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 5.11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -2006,19 +2028,18 @@
CODE_SIGN_ENTITLEMENTS = "Earthquake Network/Earthquake Network.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 146;
CURRENT_PROJECT_VERSION = "$(inherited)";
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = WJA4MR4CPC;
ENABLE_MODULE_VERIFIER = YES;
GCC_PREFIX_HEADER = "Earthquake Network/Earthquake Network-Prefix.pch";
INFOPLIST_FILE = "Earthquake Network/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.9.0;
MARKETING_VERSION = "$(inherited)";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
OTHER_LDFLAGS = (
"$(inherited)",
@@ -2082,7 +2103,6 @@
SWIFT_OBJC_BRIDGING_HEADER = "Earthquake Network/Earthquake Network-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
@@ -2094,18 +2114,17 @@
CODE_SIGN_ENTITLEMENTS = "Earthquake Network/Earthquake Network.entitlements";
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 146;
CURRENT_PROJECT_VERSION = "$(inherited)";
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = WJA4MR4CPC;
ENABLE_MODULE_VERIFIER = YES;
GCC_PREFIX_HEADER = "Earthquake Network/Earthquake Network-Prefix.pch";
INFOPLIST_FILE = "Earthquake Network/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.9.0;
MARKETING_VERSION = "$(inherited)";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
OTHER_LDFLAGS = (
"$(inherited)",
@@ -2168,7 +2187,6 @@
PROVISIONING_PROFILE_SPECIFIER = "EQNetwork - AppStore";
SWIFT_OBJC_BRIDGING_HEADER = "Earthquake Network/Earthquake Network-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
@@ -2179,7 +2197,7 @@
CODE_SIGN_ENTITLEMENTS = EQNNotificationContent/EQNNotificationContent.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 146;
CURRENT_PROJECT_VERSION = "$(inherited)";
DEVELOPMENT_TEAM = WJA4MR4CPC;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
@@ -2188,13 +2206,12 @@
"NOTIFICATION_CONTENT=1",
);
INFOPLIST_FILE = EQNNotificationContent/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.9.0;
MARKETING_VERSION = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = com.finazzi.distquake.notificationcontent;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "EQNetwork Extension Content - Development";
@@ -2202,7 +2219,6 @@
SWIFT_OBJC_BRIDGING_HEADER = "EQNNotificationContent/EQNNotificationContent-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
@@ -2213,27 +2229,25 @@
CODE_SIGN_ENTITLEMENTS = EQNNotificationContent/EQNNotificationContent.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 146;
CURRENT_PROJECT_VERSION = "$(inherited)";
DEVELOPMENT_TEAM = WJA4MR4CPC;
GCC_PREPROCESSOR_DEFINITIONS = (
"ADS_ENABLED=0",
"NOTIFICATION_CONTENT=1",
);
INFOPLIST_FILE = EQNNotificationContent/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.9.0;
MARKETING_VERSION = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = com.finazzi.distquake.notificationcontent;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "EQNetwork Extension Content - AppStore";
SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = "EQNNotificationContent/EQNNotificationContent-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
@@ -2284,7 +2298,7 @@
repositoryURL = "https://github.com/andreabusi-it/Shogun.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
minimumVersion = 2.0.0;
};
};
65B16E1C2BDFA88D0020527E /* XCRemoteSwiftPackageReference "Solar" */ = {
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27",
"version" : "1.2024011602.0"
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
@@ -42,8 +42,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk.git",
"state" : {
"revision" : "075679d6b25b28f4cb167f1d7769b58fb556fb30",
"version" : "11.8.0"
"revision" : "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5",
"version" : "11.15.0"
}
},
{
"identity" : "google-ads-on-device-conversion-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
"state" : {
"revision" : "428d8bb138e00f9a3f4f61cc6cd8863607524f65",
"version" : "2.1.0"
}
},
{
@@ -51,8 +60,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "be0881ff728eca210ccb628092af400c086abda3",
"version" : "11.7.0"
"revision" : "45ce435e9406d3c674dd249a042b932bee006f60",
"version" : "11.15.0"
}
},
{
@@ -69,8 +78,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
"version" : "8.0.2"
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
"version" : "8.1.0"
}
},
{
@@ -78,8 +87,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "f56d8fc3162de9a498377c7b6cea43431f4f5083",
"version" : "1.65.1"
"revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71",
"version" : "1.69.0"
}
},
{
@@ -96,8 +105,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "2d12673670417654f08f5f90fdd62926dc3a2648",
"version" : "100.0.0"
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
@@ -130,10 +139,10 @@
{
"identity" : "shogun",
"kind" : "remoteSourceControl",
"location" : "https://github.com/andreabusi-it/Shogun",
"location" : "https://github.com/andreabusi-it/Shogun.git",
"state" : {
"revision" : "ad890190d6be90f7712c2e56a38ef0937d9f8c1a",
"version" : "1.8.0"
"revision" : "809b56a43fadac72db9963a21c74688af7ef51b7",
"version" : "2.1.0"
}
},
{
+10 -14
View File
@@ -207,20 +207,16 @@
- (void)configureAppTracking
{
if (@available(iOS 14, *)) {
// add a delay otherwise the alert will not be displayed
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
if (status == ATTrackingManagerAuthorizationStatusAuthorized) {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = YES;
} else {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = NO;
}
}];
});
} else {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = YES;
}
// add a delay otherwise the alert will not be displayed
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
if (status == ATTrackingManagerAuthorizationStatusAuthorized) {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = YES;
} else {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = NO;
}
}];
});
}
- (void)configureFirebase
@@ -17,6 +17,7 @@ extension UserDefaults {
// Impostazioni della sezione `Allerta in tempo reale`
static let AllertaSismicaAbilitato = "NOTIFICHE_ALLERA_SISMICA_ABILITATO"
static let AllertaSismicaSuonoDisabilitatoSismaDebole = "NOTIFICHE_ALLERA_SISMICA_SUONO_DISABILITATO_SISMA_DEBOLE"
static let AllertaSismicaCriticalAlerts = "NOTIFICHE_ALLERA_SISMICA_CRITICAL_ALERTS"
static let AllertaSismicaSismiDaNotificare = "NOTIFICHE_ALLERA_SISMICA_SISMI_DA_NOTIFICARE"
static let AllertaSismicaRaggioSismiLievi = "NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_LIEVI"
@@ -58,6 +59,10 @@ extension UserDefaults {
static let AlertsShowCardOptions = "EQNetwork.AlertsShowAllCards"
/// Indica lo stile di pin da visualizzare nelle mappe
static let MapPinStyle = "EQNetwork.MapPinStyle"
/// Indica le informazioni da visualizzare nelle card `small` e `full` nella Lista Sismi
static let SeismicNetworksCardInformations = "EQNetwork.SeismicInformations";
/// Indica la tipologia di card da visualizzare nella Lista Sismi
static let SeismicNetworksCardStyle = "EQNetwork.SeismicNetworksCardStyle"
// Migrazioni
static let AppMigrationV5_3 = "EQNUserDefaultMigrationV5_3"
@@ -65,6 +70,7 @@ extension UserDefaults {
static let AppMigrationV5_8 = "EQNUserDefaultMigrationV5_8"
static let AppMigrationV5_8_2 = "EQNUserDefaultMigrationV5_8_2"
static let AppMigrationV5_9 = "EQNUserDefaultMigrationV5_9"
static let AppMigrationV5_10 = "EQNUserDefaultMigrationV5_10"
static let SettingsSeismicNetworkNotificationMigrationV5_8 = "EQNUserDefaultSettingsSeismicNetworkNotificationMigrationV5_8"
static let SettingsUserReportNotificationMigrationV5_8 = "EQNUserDefaultSettingsUserReportNotificationMigrationV5_8"
@@ -100,8 +100,10 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
- (void)setupUI
{
self.title = [NSLocalizedString(@"tab_network", nil) capitalizedString];
self.tableView.estimatedRowHeight = 200.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.contentInset = EQNBaseContainerTableViewCell.EdgeInsets;
[self.tableView registerClass:[AlertsSmartphoneNetworkTableViewCell class] forCellReuseIdentifier:@"SmartphoneNetworkCell"];
[self.tableView registerClass:[AlertsPriorityServiceTableViewCell class] forCellReuseIdentifier:@"PriorityCell"];
[self.tableView registerClass:[AlertsNoLocationTableViewCell class] forCellReuseIdentifier:@"NoLocationCell"];
@@ -147,7 +149,7 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
[self.tableItems addObject:@(AllerteTableRowReteSmartphone)];
}
// check if locations is enabled
if (CLLocationManager.authorizationStatus != kCLAuthorizationStatusAuthorizedAlways) {
if (EQNUserData.sharedData.locationAuthorizationStatus != kCLAuthorizationStatusAuthorizedAlways) {
[self.tableItems addObject:@(AllerteTableRowLocationPermission)];
}
@@ -233,7 +235,7 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
- (void)actionTestPush
{
CLAuthorizationStatus status = CLLocationManager.authorizationStatus;
CLAuthorizationStatus status = EQNUserData.sharedData.locationAuthorizationStatus;
if (status != kCLAuthorizationStatusAuthorizedAlways && status != kCLAuthorizationStatusAuthorizedWhenInUse) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"attention", nil)
message:NSLocalizedString(@"liveview_unknown_location", nil)
@@ -271,7 +273,7 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
if (tableRow == AllerteTableRowLocationPermission) {
AlertsNoLocationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"NoLocationCell" forIndexPath:indexPath];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell updateWith:CLLocationManager.authorizationStatus];
[cell updateWith:EQNUserData.sharedData.locationAuthorizationStatus];
return cell;
} else if (tableRow == AllerteTableRowSismiRilevati) {
@@ -7,7 +7,7 @@
//
import UIKit
import Shogun
@objc
class AlertsSmartphoneNetworkTableViewCell: EQNBaseContainerTableViewCell {
@@ -22,7 +22,7 @@ class AlertsSmartphoneNetworkTableViewCell: EQNBaseContainerTableViewCell {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.Colors.green
label.font = .preferredFont(forTextStyle: .largeTitle)
label.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
label.textAlignment = .center
return label
}()
@@ -39,7 +39,7 @@ class SubscriptionDetailsViewController: UITableViewController {
products: [EQNInAppProducts]
) {
self.products = products
self.selectedProduct = products.first(where: { $0.plan == .monthly }) ?? products.first!
self.selectedProduct = products.first(where: { $0.plan == .yearly }) ?? products.first!
super.init(style: .plain)
}
@@ -67,6 +67,7 @@ class SubscriptionDetailsViewController: UITableViewController {
tableView.estimatedRowHeight = 2000.0
tableView.separatorStyle = .none
tableView.backgroundColor = .systemGroupedBackground
tableView.contentInset = EQNBaseContainerTableViewCell.EdgeInsets
tableView.registerCell(for: SubscriptionDetailsTableViewCell.self)
}
@@ -18,6 +18,7 @@ class SubscriptionsHeaderTableViewCell: UITableViewHeaderFooterView {
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.preferredFont(forTextStyle: .title2)
label.textColor = AppTheme.Colors.darkGray
label.textAlignment = .center
return label
}()
@@ -48,6 +49,7 @@ class SubscriptionsHeaderTableViewCell: UITableViewHeaderFooterView {
headerTitleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
headerTitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: .cardPadding).isActive = true
headerTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: .cardPadding.negative).isActive = true
loadingActivityIndicator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: .cardPadding.negative).isActive = true
loadingActivityIndicator.centerYAnchor.constraint(equalTo: headerTitleLabel.centerYAnchor).isActive = true
}
@@ -91,6 +91,9 @@ class SubscriptionsViewController: UITableViewController {
tableView.estimatedRowHeight = 600.0
tableView.separatorStyle = .none
tableView.backgroundColor = .systemGroupedBackground
tableView.contentInset = EQNBaseContainerTableViewCell.EdgeInsets
// remove extra padding on top of each section header
tableView.sectionHeaderTopPadding = 0.0
tableView.registerCell(for: SubscriptionsActiveTableViewCell.self)
tableView.registerCell(for: SubscriptionsDescriptionTableViewCell.self)
tableView.registerCell(for: SubscriptionProductTableViewCell.self)
@@ -114,14 +117,21 @@ class SubscriptionsViewController: UITableViewController {
let isSubscription = product.isSubscription
return isPurchased && isSubscription
}.sorted { lProduct, rProduct in
// if user has more than one subscriptions,
// show first the Top10k
let lIs10k = lProduct.isTop100k
let rIs10k = rProduct.isTop100k
if lIs10k {
return lIs10k
// If user has more than one subscriptions,
// show first the Top10k.
let lIs10k = lProduct.isTop10k
let rIs10k = rProduct.isTop10k
// If left item is Top10k, order first
if lIs10k && !rIs10k {
return true
} else if !lProduct.isTop10k && rProduct.isTop10k {
// right product is Top10k, left no
return false
} else {
// both products Top10k or Top100k, keep existing order
return false
}
return rIs10k
}
self.productSubscribed = purchased.first
self.products = products.sorted(by: { $0.productIdentifier > $1.productIdentifier })
@@ -190,17 +200,22 @@ class SubscriptionsViewController: UITableViewController {
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let tableSection = sections[section]
let view = tableView.dequeueHeaderFooterView(cellIdentifiable: SubscriptionsHeaderTableViewCell.self)
view.update(isLoading: isLoading, title: tableSection.sectionTitle)
return view
switch tableSection.sectionTitle {
case .some(let title):
let view = tableView.dequeueHeaderFooterView(cellIdentifiable: SubscriptionsHeaderTableViewCell.self)
view.update(isLoading: isLoading, title: title)
return view
case .none:
return nil
}
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let tableSection = sections[section]
if tableSection.sectionTitle != nil {
return 50
return switch tableSection.sectionTitle {
case .some: 50.0
case .none: 0.0
}
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -0,0 +1,187 @@
//
// MapSeismicWaveAnimator.swift
// Earthquake Network
//
// Created by Andrea Busi on 24/07/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import UIKit
import MapKit
class MapSeismicWaveAnimator {
private weak var mapView: MKMapView?
private weak var waveTimeLabel: UILabel?
private let OverlayCircleId = "wave_animation"
/// Alert to display
private let realtimeAlert: EQNRealtimePushNotification
/// Timer to constantly update countdown label
private var countdownTimer: Timer?
/// Timer to simulate animation for the wave
private var waveAnimationTimer: 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
// MARK: - Init
init(
realtimeAlert: EQNRealtimePushNotification,
mapView: MKMapView,
waveTimeLabel: UILabel
) {
self.realtimeAlert = realtimeAlert
self.mapView = mapView
self.waveTimeLabel = waveTimeLabel
self.setup()
}
private func setup() {
self.waveAnimationCurrentRadius = currentWavePosition()
self.waveAnimationVelocity = evaluateWaveAnimationVelocity()
}
// MARK: - Public
func start() {
startCountdown()
startWaveAnimation()
}
func stop() {
stopCountdown()
stopWaveAnimation()
}
// MARK: - Wave
private func startCountdown() {
// show countdown only if time is less than 300 seconds
if realtimeAlert.currentCountdown() < 300 {
// start a timer for the countdown label
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()
}
private func stopCountdown() {
countdownTimer?.invalidate()
countdownTimer = nil
}
private func stopWaveAnimation() {
waveAnimationTimer?.invalidate()
waveAnimationTimer = nil
}
// MARK: - Timer
@objc private func countdownTimerFired(_ sender: Timer) {
let countdown = realtimeAlert.currentCountdown()
waveTimeLabel?.text = String.localizedStringWithFormat(NSLocalizedString("alert_wave", comment: ""), countdown)
waveTimeLabel?.textColor = waveTimeTextColor(for: countdown)
if countdown <= 0 {
// stop the countdown
stopCountdown()
}
}
@objc private func mapWaveAnimationFired(_ sender: Timer) {
waveAnimationCurrentRadius += waveAnimationVelocity
addMapCircle(center: realtimeAlert.coordinate.coordinate, radius: waveAnimationCurrentRadius)
}
// MARK: - Helpers
/// 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 {
// distanza tra utente e terremoto
let distance = realtimeAlert.distanceFromUser()
// calcoliamo la distanza rimanente da mostrare, perchè la schermata potrebbe anche essere aperta in ritardo
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 evaluateWaveAnimationVelocity() -> Double {
let velocity = realtimeAlert.waveSpeed
return velocity * waveAnimationRefreshRate
}
/// Returns the text color based on impact countdown
private func waveTimeTextColor(for countdown: Int) -> UIColor {
switch countdown {
case _ where countdown > 15:
return UIColor(red: 255.0/255.0, green: 140.0/255.0, blue: 0.0, alpha: 1.0)
case _ where countdown > 5:
return UIColor(red: 255.0/255.0, green: 100.0/255.0, blue: 0.0, alpha: 1.0)
default:
return UIColor(red: 255.0/255.0, green: 0.0/255.0, blue: 0.0, alpha: 1.0)
}
}
// MARK: - Map management
func addMapCircle(
center: CLLocationCoordinate2D,
radius: CLLocationDistance
) {
guard let mapView else { return }
// remove any other existing overlays
let overlays = mapView.overlays.filter { $0.title == OverlayCircleId }
mapView.removeOverlays(overlays)
// add new overlay
let circle = MKCircle(center: center, radius: radius)
circle.title = OverlayCircleId
mapView.addOverlay(circle)
}
func getOverlayRenderer(for overlay: MKOverlay) -> MKOverlayRenderer? {
switch overlay {
case let circle as MKCircle where overlay.title == OverlayCircleId:
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 nil
}
}
}
@@ -102,7 +102,6 @@ class RealtimeAlertView: UIView {
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
@@ -160,28 +159,13 @@ class RealtimeAlertView: UIView {
}
// MARK: - Public
func addMapCircle(
center: CLLocationCoordinate2D,
radius: CLLocationDistance,
overlayId: String
func addMapLine(
coordinates: [CLLocationCoordinate2D]
) {
// 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)
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
mapView.addOverlay(polyline)
}
func addMapLine(
coordinates: [CLLocationCoordinate2D]
) {
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
mapView.addOverlay(polyline)
}
func addMapAnnotation(
title: String = "",
@@ -192,35 +176,3 @@ class RealtimeAlertView: UIView {
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
}
}
@@ -10,7 +10,7 @@ import UIKit
import MapKit
class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
class RealtimeAlertViewController: UIViewController {
@objc var onClose: () -> Void = {}
@@ -20,17 +20,17 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
private var notificationView: RealtimeAlertView {
containerView.alertView
}
/// Manage the wave animation on the map and the countdown label
private lazy var animator: MapSeismicWaveAnimator = {
let animator = MapSeismicWaveAnimator(
realtimeAlert: realtimeAlert,
mapView: notificationView.mapView,
waveTimeLabel: notificationView.waveTimeLabel
)
return animator
}()
/// Alert to display
private let realtimeAlert: EQNRealtimePushNotification
/// 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?
// MARK: - Init
@@ -38,9 +38,6 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
init(notification: EQNRealtimePushNotification) {
self.realtimeAlert = notification
super.init(nibName: nil, bundle: nil)
self.waveAnimationCurrentRadius = currentWavePosition()
self.waveAnimationVelocity = evaluateWaveAnimationVelocity()
}
required init?(coder: NSCoder) {
@@ -64,8 +61,7 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
configureUI()
updateUI()
startCountdown()
startWaveAnimation()
animator.start()
}
override func viewWillAppear(_ animated: Bool) {
@@ -77,6 +73,8 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
// MARK: - Private
private func configureUI() {
notificationView.mapView.delegate = self
notificationView.closeButton.addTarget(self, action: #selector(onTapClose(_:)), for: .touchUpInside)
// configure color for animation
@@ -104,93 +102,37 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
// aggiungiamo annotation con epicentro sisma
notificationView.addMapAnnotation(center: realtimeAlert.coordinate.coordinate, intensity: realtimeAlert.intensity)
// simuliamo animazione dell'onda sismica
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: [realtimeAlert.coordinate.coordinate, lastPosition.coordinate])
}
}
private func startCountdown() {
// show countdown only if time is less than 300 seconds
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)
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) {
// invalidiamo i timer, altri
countdownTimer?.invalidate()
countdownTimer = nil
waveAnimationTimer?.invalidate()
waveAnimationTimer = nil
// stoppiamo animazione e countdown
animator.stop()
onClose()
dismiss(animated: true)
}
}
extension RealtimeAlertViewController: MKMapViewDelegate {
// MARK: - Timer
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
animator.getOverlayRenderer(for: overlay) ?? MKOverlayRenderer(overlay: overlay)
}
@objc private func countdownTimerFired(_ sender: Timer) {
let countdown = realtimeAlert.currentCountdown()
notificationView.waveTimeLabel.text = String.localizedStringWithFormat(NSLocalizedString("alert_wave", comment: ""), countdown)
notificationView.waveTimeLabel.textColor = waveTimeTextColor(for: countdown)
if countdown <= 0 {
// stop the countdown
countdownTimer?.invalidate()
countdownTimer = nil
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? EQNMapAnnotationPastquake else {
return nil
}
}
@objc private func mapWaveAnimationFired(_ sender: Timer) {
waveAnimationCurrentRadius += waveAnimationVelocity
notificationView.addMapCircle(center: realtimeAlert.coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation")
}
// MARK: - Helpers
/// 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 {
// distanza tra utente e terremoto
let distance = realtimeAlert.distanceFromUser()
// calcoliamo la distanza rimanente da mostrare, perchè la schermata potrebbe anche essere aperta in ritardo
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 evaluateWaveAnimationVelocity() -> Double {
let velocity = realtimeAlert.waveSpeed
return velocity * waveAnimationRefreshRate
}
/// Returns the text color based on impact countdown
private func waveTimeTextColor(for countdown: Int) -> UIColor {
switch countdown {
case _ where countdown > 15:
return UIColor(red: 255.0/255.0, green: 140.0/255.0, blue: 0.0, alpha: 1.0)
case _ where countdown > 5:
return UIColor(red: 255.0/255.0, green: 100.0/255.0, blue: 0.0, alpha: 1.0)
default:
return UIColor(red: 255.0/255.0, green: 0.0/255.0, blue: 0.0, alpha: 1.0)
}
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: EQNCustomAnnotationView.SingleLineIdentifier, for: annotation) as! EQNCustomAnnotationView
annotationView.image = annotation.image
annotationView.title = annotation.title
return annotationView
}
}
@@ -16,7 +16,7 @@ class SegnalazioniLast24HoursCell: EQNBaseContainerTableViewCell {
@objc var onTapMap: (() -> Void)?
@objc var onTapTelegram: (() -> Void)?
override var headerText: String { NSLocalizedString("tab_manual", comment: "") }
override var isHeaderVisible: Bool { false }
// MARK: - UI
@@ -24,7 +24,7 @@ class SegnalazioniLast24HoursCell: EQNBaseContainerTableViewCell {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.Colors.red
label.font = .preferredFont(forTextStyle: .largeTitle)
label.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
label.textAlignment = .center
label.numberOfLines = 0
return label
@@ -43,7 +43,7 @@ class SegnalazioniLast24HoursCell: EQNBaseContainerTableViewCell {
private lazy var twitterButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(twitterButtonTapped(_:)))
button.imageView?.contentMode = .scaleAspectFit
button.setImage(.init(named: "twitter_icon"), for: .normal)
button.setImage(.init(named: "xcorp_icon"), for: .normal)
return button
}()
@@ -38,8 +38,10 @@
- (void)setupUI
{
self.title = [NSLocalizedString(@"tab_manual", nil) capitalizedString];
self.tableView.estimatedRowHeight = 500.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.contentInset = EQNBaseContainerTableViewCell.EdgeInsets;
[self.tableView registerClass:[SegnalazioniLast24HoursCell class] forCellReuseIdentifier:@"Last24HCell"];
[self.tableView registerClass:[SegnalazioniSendReportCell class] forCellReuseIdentifier:@"ReportEarthquakeCell"];
}
@@ -0,0 +1,127 @@
//
// SeismicNetworkBaseTableViewCell.swift
// Earthquake Network
//
// Created by Andrea Busi on 06/03/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import UIKit
import Shogun
protocol SeismicNetworkBaseTableViewCellDelegate: AnyObject {
func seismicNetworkCellDidTapShare(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapIntensityMapDetail(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapCalendar(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapSettings(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkBaseTableViewCell)
}
class SeismicNetworkBaseTableViewCell: UITableViewCell {
/// Delegate
weak var delegate: SeismicNetworkBaseTableViewCellDelegate?
/// Available informations to display inside the cell
enum InformationType: Int {
case preliminary
case time
case distance
case coordinate
case population
case realtimeSmartphones
case reportUsers
case intensityMap
case buttons
}
// MARL: - Internal
static let DefaultButtonHeight: CGFloat = 34.0
static let VerticalSpacingDefault: CGFloat = 6.0
static let VerticalSpacingSmall: CGFloat = 2.0
static let HorizontalSpacingDefault: CGFloat = 4.0
// MARK: - UI Components
lazy var containerView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.clipsToBounds = true
return view
}()
lazy var gradientView: UIImageView = {
// Per gestire il gradiente, utilizziamo una image view in cui inseriamo un'immagine
// creata ad-hoc con il gradiente desiderato.
// Le prove fatte utilizzando una view normale sono fallite perchè al momento di
// disegnare la view non abbiamo le misure corrette.
let view = UIImageView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleToFill
return view
}()
// MARK: - Init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
// MARK: - View Lifecycle
override func layoutSubviews() {
super.layoutSubviews()
containerView.eqn_applyShadowAndRoundedCorners()
gradientView.eqn_applyRoundedCorners()
}
// MARK: - Setup
func setupUI() {
selectionStyle = .default
backgroundColor = .clear
// container view
contentView.addSubview(containerView)
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4.0).isActive = true
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0).isActive = true
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0).isActive = true
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4.0).isActive = true
containerView.addSubview(gradientView)
gradientView.constraint(to: containerView)
}
func recreateUI() {
// remove all subviews and recreate the required components
containerView.subviews.forEach({ $0.removeFromSuperview() })
setupUI()
}
@discardableResult
func addSeparator(constraintTo: NSLayoutYAxisAnchor, constanst: CGFloat = 8.0) -> UIView {
let separator = UIView()
separator.translatesAutoresizingMaskIntoConstraints = false
separator.backgroundColor = .lightGray
containerView.addSubview(separator)
separator.topAnchor.constraint(equalTo: constraintTo, constant: constanst).isActive = true
separator.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
separator.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
separator.heightAnchor.constraint(equalToConstant: 1.0).isActive = true
return separator
}
}
@@ -0,0 +1,233 @@
//
// SeismicNetworkMinimalTableViewCell.swift
// Earthquake Network
//
// Created by Andrea Busi on 06/03/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import UIKit
import Shogun
class SeismicNetworkMinimalTableViewCell: SeismicNetworkBaseTableViewCell {
// MARK: - UI
private lazy var magnitudeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.preferredFont(forTextStyle: .largeTitle)
label.textColor = .red
label.textAlignment = .center
return label
}()
private lazy var placeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.preferredFont(forTextStyle: .title2, weight: .semibold)
label.numberOfLines = 3
return label
}()
private lazy var timeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var distanceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var smartphonesLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
private lazy var alertsLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
// MARK: - Internal
/// Seismic to show
private var seismic: EQNSisma?
private var isPushSelected = false
private var informationTypes: Set<InformationType> = []
// MARK: - Setup
override func setupUI() {
super.setupUI()
// this variable is used to keep track of the previous view, in order to attach proper constraints
var previousView: UIView = containerView
// preliminary banner on top of the cell
if informationTypes.contains(.preliminary) {
let preliminaryLabel = UILabel()
preliminaryLabel.translatesAutoresizingMaskIntoConstraints = false
preliminaryLabel.text = NSLocalizedString("official_prelimiary", comment: "").uppercased()
preliminaryLabel.textAlignment = .center
preliminaryLabel.backgroundColor = .red
preliminaryLabel.textColor = .yellow
containerView.addSubview(preliminaryLabel)
preliminaryLabel.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
preliminaryLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
preliminaryLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
preliminaryLabel.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
previousView = preliminaryLabel
}
containerView.addSubview(magnitudeLabel)
containerView.addSubview(placeLabel)
let titleTopAnchor = previousView == containerView ? containerView.layoutMarginsGuide.topAnchor : previousView.bottomAnchor
let stackViewInformations = UIStackView(arrangedSubviews: [timeLabel, distanceLabel])
stackViewInformations.translatesAutoresizingMaskIntoConstraints = false
stackViewInformations.axis = .horizontal
stackViewInformations.distribution = .fillEqually
stackViewInformations.spacing = Self.HorizontalSpacingDefault
containerView.addSubview(stackViewInformations)
let stackViewRight = UIStackView(arrangedSubviews: [placeLabel, stackViewInformations])
stackViewRight.translatesAutoresizingMaskIntoConstraints = false
stackViewRight.axis = .vertical
stackViewRight.distribution = .equalSpacing
stackViewRight.spacing = Self.VerticalSpacingDefault
let stackViewMain = UIStackView(arrangedSubviews: [magnitudeLabel, stackViewRight])
stackViewMain.translatesAutoresizingMaskIntoConstraints = false
stackViewMain.axis = .horizontal
stackViewMain.distribution = .fill
stackViewMain.spacing = Self.HorizontalSpacingDefault
containerView.addSubview(stackViewMain)
stackViewMain.topAnchor.constraint(equalTo: titleTopAnchor).isActive = true
stackViewMain.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
stackViewMain.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
magnitudeLabel.widthAnchor.constraint(equalToConstant: 60.0).isActive = true
previousView = stackViewMain
if informationTypes.contains(.realtimeSmartphones) || informationTypes.contains(.reportUsers) || informationTypes.contains(.intensityMap) {
let separator = addSeparator(constraintTo: previousView.bottomAnchor, constanst: Self.VerticalSpacingDefault)
let stackViewReports = UIStackView()
stackViewReports.translatesAutoresizingMaskIntoConstraints = false
stackViewReports.axis = .vertical
stackViewReports.distribution = .equalSpacing
stackViewReports.alignment = .center
stackViewReports.spacing = Self.VerticalSpacingDefault
if informationTypes.contains(.realtimeSmartphones) {
stackViewReports.addArrangedSubview(smartphonesLabel)
}
if informationTypes.contains(.reportUsers) {
stackViewReports.addArrangedSubview(alertsLabel)
}
if informationTypes.contains(.intensityMap) {
let buttonMap = EQNRoundedButton.make(title: "🎯 \(NSLocalizedString("shakemap", comment: ""))", target: self, action: #selector(intensityMapTapped(_:)))
stackViewReports.addArrangedSubview(buttonMap)
buttonMap.heightAnchor.constraint(equalToConstant: Self.DefaultButtonHeight).isActive = true
buttonMap.leadingAnchor.constraint(equalTo: stackViewReports.leadingAnchor).isActive = true
buttonMap.trailingAnchor.constraint(equalTo: stackViewReports.trailingAnchor).isActive = true
}
containerView.addSubview(stackViewReports)
stackViewReports.topAnchor.constraint(equalTo: separator.bottomAnchor, constant: Self.VerticalSpacingDefault).isActive = true
stackViewReports.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
stackViewReports.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
previousView = stackViewReports
}
previousView.bottomAnchor.constraint(equalTo: containerView.layoutMarginsGuide.bottomAnchor).isActive = true
containerView.eqn_applyShadowAndRoundedCorners()
gradientView.eqn_applyRoundedCorners()
}
private func updateUI() {
guard let seismic = seismic else { return }
let viewModel = SeismicNetworkMinimalViewModel(seismic: seismic)
gradientView.image = .gradient(from: viewModel.colors.startColor, to: viewModel.colors.endColor, with: .init(origin: .zero, size: .init(width: 500, height: 1)))
placeLabel.text = viewModel.place
placeLabel.textColor = isPushSelected ? AppTheme.Colors.pureBlue : AppTheme.shared.cardTextColor
magnitudeLabel.textColor = viewModel.colors.textColor
magnitudeLabel.text = viewModel.magnitude
timeLabel.text = "🕗 \(viewModel.time)"
distanceLabel.text = "📐 \(viewModel.distance)"
if !viewModel.smartphones.isEmpty {
smartphonesLabel.text = "🚨 \(viewModel.smartphones)"
}
if !viewModel.users.isEmpty {
alertsLabel.text = "⚠️ \(viewModel.users)"
}
}
// MARK: - Public
/// Configure the cell to display a seismic
/// - Parameters:
/// - seismic: Seismic to display
/// - type: Type of cell
/// - informations: Informations to show
public func configure(
with seismic: EQNSisma,
isPushSelected: Bool
) {
self.seismic = seismic
self.isPushSelected = isPushSelected
self.informationTypes.removeAll()
if seismic.preliminary.intValue > 0 {
informationTypes.insert(.preliminary)
}
if seismic.smartphoneNumber.intValue > 0 {
informationTypes.insert(.realtimeSmartphones)
}
if seismic.userNumber.intValue > 0 {
informationTypes.insert(.reportUsers)
}
if seismic.isoCode != "0" {
informationTypes.insert(.intensityMap)
}
recreateUI()
updateUI()
}
// MARK: - Actions
@objc private func intensityMapTapped(_ sender: Any) {
delegate?.seismicNetworkCellDidTapIntensityMapDetail(self)
}
}
@@ -11,32 +11,8 @@ import MapKit
import CoreLocation
import Shogun
protocol SeismicNetworkTableViewCellDelegate: AnyObject {
func seismicNetworkCellDidTapShare(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapIntensityMapDetail(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapCalendar(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapSettings(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkTableViewCell)
}
class SeismicNetworkTableViewCell: UITableViewCell {
static let Identifier = "SeismicNetworkTableViewCell"
/// Available informations to display inside the cell
enum InformationType: Int {
case preliminary
case time
case distance
case coordinate
case population
case realtimeSmartphones
case reportUsers
case intensityMap
case buttons
}
class SeismicNetworkTableViewCell: SeismicNetworkBaseTableViewCell {
/// Available cell type
enum DisplayType {
@@ -45,14 +21,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
/// Cell with map visible
case mapExpanded
}
/// Delegate
weak var delegate: SeismicNetworkTableViewCellDelegate?
// MARK: - Internal
private static let DefaultButtonHeight: CGFloat = 30.0
private static let DefaultVerticalSpacing: CGFloat = 6.0
/// Seismic to show
private var seismic: EQNSisma?
@@ -61,26 +29,7 @@ class SeismicNetworkTableViewCell: UITableViewCell {
private var isPushSelected = false
// MARK: - UI Components
private lazy var containerView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.clipsToBounds = true
return view
}()
private lazy var gradientView: UIImageView = {
// Per gestire il gradiente, utilizziamo una image view in cui inseriamo un'immagine
// creata ad-hoc con il gradiente desiderato.
// Le prove fatte utilizzando una view normale sono fallite perchè al momento di
// disegnare la view non abbiamo le misure corrette.
let view = UIImageView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleToFill
return view
}()
private lazy var placeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
@@ -89,6 +38,14 @@ class SeismicNetworkTableViewCell: UITableViewCell {
return label
}()
private lazy var shareButton: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(named: "share_icon"), for: .normal)
button.addTarget(self, action: #selector(shareTapped(_:)), for: .touchUpInside)
return button
}()
private lazy var networkLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
@@ -199,20 +156,9 @@ class SeismicNetworkTableViewCell: UITableViewCell {
// MARK: - Setup
private func setupUI() {
selectionStyle = .default
backgroundColor = .clear
// container view
contentView.addSubview(containerView)
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4.0).isActive = true
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0).isActive = true
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0).isActive = true
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4.0).isActive = true
override func setupUI() {
super.setupUI()
containerView.addSubview(gradientView)
gradientView.constraint(to: containerView)
// this variable is used to keep track of the previous view, in order to attach proper constraints
var previousView: UIView = containerView
@@ -233,41 +179,27 @@ class SeismicNetworkTableViewCell: UITableViewCell {
previousView = preliminaryLabel
}
// title (bell icon, place label, seismic network and share button)
let titleComponentsHeight: CGFloat = 30.0
let stackViewTitle = UIStackView()
stackViewTitle.translatesAutoresizingMaskIntoConstraints = false
stackViewTitle.axis = .horizontal
stackViewTitle.distribution = .fill
stackViewTitle.alignment = .center
stackViewTitle.spacing = 4
let shareButton = UIButton(type: .custom)
shareButton.setImage(UIImage(named: "share_icon"), for: .normal)
shareButton.addTarget(self, action: #selector(shareTapped(_:)), for: .touchUpInside)
stackViewTitle.addArrangedSubview(placeLabel)
stackViewTitle.addArrangedSubview(shareButton)
placeLabel.setContentHuggingPriority(.init(200), for: .horizontal)
placeLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
shareButton.widthAnchor.constraint(equalToConstant: titleComponentsHeight).isActive = true
shareButton.widthAnchor.constraint(equalTo: shareButton.heightAnchor).isActive = true
containerView.addSubview(placeLabel)
containerView.addSubview(shareButton)
let titleTopAnchor = previousView == containerView ? containerView.layoutMarginsGuide.topAnchor : previousView.bottomAnchor
containerView.addSubview(stackViewTitle)
stackViewTitle.topAnchor.constraint(equalTo: titleTopAnchor).isActive = true
stackViewTitle.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
stackViewTitle.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
placeLabel.topAnchor.constraint(equalTo: titleTopAnchor).isActive = true
placeLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
placeLabel.trailingAnchor.constraint(equalTo: shareButton.leadingAnchor, constant: .cardPadding.negative).isActive = true
shareButton.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
shareButton.centerYAnchor.constraint(equalTo: placeLabel.centerYAnchor).isActive = true
shareButton.heightAnchor.constraint(equalToConstant: 24.0).isActive = true
shareButton.heightAnchor.constraint(equalTo: shareButton.widthAnchor, multiplier: 1.0).isActive = true
let separator1 = addSeparator(constraintTo: stackViewTitle.bottomAnchor)
let separator1 = addSeparator(constraintTo: placeLabel.bottomAnchor)
let informationsLeadingAnchor = separator1.leadingAnchor
let informationsTrailingAnchor = separator1.trailingAnchor
// magnitude information
containerView.addSubview(magnitudeLabel)
magnitudeLabel.topAnchor.constraint(equalTo: separator1.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
magnitudeLabel.topAnchor.constraint(equalTo: separator1.bottomAnchor, constant: Self.VerticalSpacingSmall).isActive = true
magnitudeLabel.leadingAnchor.constraint(equalTo: informationsLeadingAnchor, constant: 14).isActive = true
if !informationTypes.contains(.preliminary) {
@@ -297,27 +229,27 @@ class SeismicNetworkTableViewCell: UITableViewCell {
}
containerView.addSubview(stackViewInformations)
stackViewInformations.topAnchor.constraint(equalTo: magnitudeLabel.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
stackViewInformations.topAnchor.constraint(equalTo: magnitudeLabel.bottomAnchor, constant: Self.VerticalSpacingSmall).isActive = true
stackViewInformations.leadingAnchor.constraint(equalTo: informationsLeadingAnchor, constant: 14).isActive = true
stackViewInformations.trailingAnchor.constraint(equalTo: informationsTrailingAnchor, constant: -14).isActive = true
previousView = stackViewInformations
// network
containerView.addSubview(networkLabel)
networkLabel.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
networkLabel.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.VerticalSpacingSmall).isActive = true
networkLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
networkLabel.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
previousView = networkLabel
if informationTypes.contains(.realtimeSmartphones) || informationTypes.contains(.reportUsers) || informationTypes.contains(.intensityMap) {
let separator2 = addSeparator(constraintTo: previousView.bottomAnchor)
let separator2 = addSeparator(constraintTo: previousView.bottomAnchor, constanst: Self.VerticalSpacingSmall)
let stackViewReports = UIStackView()
stackViewReports.translatesAutoresizingMaskIntoConstraints = false
stackViewReports.axis = .vertical
stackViewReports.distribution = .equalSpacing
stackViewReports.alignment = .center
stackViewReports.spacing = Self.DefaultVerticalSpacing
stackViewReports.spacing = Self.VerticalSpacingDefault
if informationTypes.contains(.realtimeSmartphones) {
stackViewReports.addArrangedSubview(smartphonesLabel)
@@ -334,15 +266,17 @@ class SeismicNetworkTableViewCell: UITableViewCell {
}
containerView.addSubview(stackViewReports)
stackViewReports.topAnchor.constraint(equalTo: separator2.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
stackViewReports.topAnchor.constraint(equalTo: separator2.bottomAnchor, constant: Self.VerticalSpacingDefault).isActive = true
stackViewReports.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
stackViewReports.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
let separator3 = addSeparator(constraintTo: stackViewReports.bottomAnchor)
previousView = separator3
previousView = stackViewReports
}
if informationTypes.contains(.buttons) {
let separator3 = addSeparator(constraintTo: previousView.bottomAnchor)
previousView = separator3
// buttons
let stackViewButtons = UIStackView()
stackViewButtons.translatesAutoresizingMaskIntoConstraints = false
@@ -359,7 +293,7 @@ class SeismicNetworkTableViewCell: UITableViewCell {
containerView.addSubview(stackViewButtons)
stackViewButtons.heightAnchor.constraint(equalToConstant: Self.DefaultButtonHeight).isActive = true
stackViewButtons.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
stackViewButtons.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.VerticalSpacingDefault).isActive = true
stackViewButtons.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
stackViewButtons.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
@@ -369,7 +303,7 @@ class SeismicNetworkTableViewCell: UITableViewCell {
if displayType == .mapExpanded {
containerView.addSubview(mapView)
mapView.heightAnchor.constraint(equalToConstant: 140.0).isActive = true
mapView.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
mapView.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.VerticalSpacingDefault).isActive = true
mapView.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
@@ -381,7 +315,7 @@ class SeismicNetworkTableViewCell: UITableViewCell {
containerView.addSubview(buttonClose)
buttonClose.heightAnchor.constraint(equalToConstant: Self.DefaultButtonHeight).isActive = true
buttonClose.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
buttonClose.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.VerticalSpacingDefault).isActive = true
buttonClose.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
buttonClose.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
buttonClose.bottomAnchor.constraint(equalTo: containerView.layoutMarginsGuide.bottomAnchor).isActive = true
@@ -394,12 +328,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
gradientView.eqn_applyRoundedCorners()
}
private func recreateUI() {
// remove all subviews and recreate the required components
containerView.subviews.forEach({ $0.removeFromSuperview() })
setupUI()
}
private func updateUI() {
guard let seismic = seismic else { return }
@@ -430,7 +358,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
alertsLabel.text = "⚠️ \(viewModel.users)"
}
if displayType == .mapExpanded {
// zoom based on population involved
let longitudeSpan = mapSpanLongitude(population: seismic.population100km)
@@ -491,52 +418,37 @@ class SeismicNetworkTableViewCell: UITableViewCell {
// MARK: - Actions
@objc func shareTapped(_ sender: UIButton) {
@objc private func shareTapped(_ sender: UIButton) {
delegate?.seismicNetworkCellDidTapShare(self)
}
@objc func mapTapped(_ sender: UIButton) {
@objc private func mapTapped(_ sender: UIButton) {
if displayType != .mapExpanded {
delegate?.seismicNetworkCellDidTapMap(self)
}
}
@objc func calendarTapped(_ sender: UIButton) {
@objc private func calendarTapped(_ sender: UIButton) {
delegate?.seismicNetworkCellDidTapCalendar(self)
}
@objc func settingsTapped(_ sender: UIButton) {
@objc private func settingsTapped(_ sender: UIButton) {
delegate?.seismicNetworkCellDidTapSettings(self)
}
@objc func closeTapped(_ sender: UIButton) {
@objc private func closeTapped(_ sender: UIButton) {
delegate?.seismicNetworkCellDidTapClose(self)
}
@objc func mapDetailTapped(_ sender: Any) {
@objc private func mapDetailTapped(_ sender: Any) {
delegate?.seismicNetworkCellDidTapMapDetail(self)
}
@objc func intensityMapTapped(_ sender: Any) {
@objc private func intensityMapTapped(_ sender: Any) {
delegate?.seismicNetworkCellDidTapIntensityMapDetail(self)
}
// MARK: - Helpers
@discardableResult
private func addSeparator(constraintTo: NSLayoutYAxisAnchor, constanst: CGFloat = 8.0) -> UIView {
let separator = UIView()
separator.translatesAutoresizingMaskIntoConstraints = false
separator.backgroundColor = .lightGray
containerView.addSubview(separator)
separator.topAnchor.constraint(equalTo: constraintTo, constant: constanst).isActive = true
separator.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
separator.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
separator.heightAnchor.constraint(equalToConstant: 1.0).isActive = true
return separator
}
/// Determines the zoom for the map, based on the involved population
private func mapSpanLongitude(population: Double) -> CLLocationDegrees {
@@ -45,7 +45,7 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
]
private let initialFilterType = EQNSeismic.shared.filterOption
private var currentFilterType = EQNSeismic.FilterType.inRadius
private(set) var currentFilterType = EQNSeismic.FilterType.inRadius
private var currentMaximumDistance = EQNData.DefaultFilterRadius
private var currentMinimumMagnitude = EQNData.DefaultFilterMagnitude
@@ -29,17 +29,16 @@ class SeismicCardSettingsViewController: UIViewController {
@IBOutlet private weak var informationPopulationSwitch: UISwitch!
@IBOutlet private weak var closeButton: UIButton!
private var informations = [SeismicNetworkTableViewCell.InformationType]()
private var informations: [SeismicNetworkTableViewCell.InformationType] {
get { AppPreferences.shared.seismicNetworksInformations }
set { AppPreferences.shared.seismicNetworksInformations = newValue }
}
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
if let saved = UserDefaults.standard.array(forKey: EQNUserDefaultKeySesmicInformations) as? [Int] {
informations = saved.compactMap { SeismicNetworkTableViewCell.InformationType(rawValue: $0) }
}
setupUI()
updateUI()
}
@@ -84,7 +83,6 @@ class SeismicCardSettingsViewController: UIViewController {
toggle(information: .population)
}
UserDefaults.standard.set(informations.map { $0.rawValue }, forKey: EQNUserDefaultKeySesmicInformations)
updateUI()
}
@@ -0,0 +1,144 @@
//
// SeismicNetworkFilterRecapView.swift
// Earthquake Network
//
// Created by Andrea Busi on 17/07/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import SwiftUI
import Shogun
struct SeismicNetworkFilterRecapView: View {
class Model: ObservableObject {
@Published var filter = EQNSeismic.shared.filterOption
@Published var sort = EQNSeismic.shared.sort
}
@ObservedObject private var model: Model
private let onSort: (_ sort: EQNSeismic.Sort) -> Void
private let onMainFilter: () -> Void
private let onMap: () -> Void
// MARK: - Init
init(
model: Model,
onSort: @escaping (_ sort: EQNSeismic.Sort) -> Void,
onMainFilter: @escaping () -> Void,
onMap: @escaping () -> Void
) {
self.model = model
self.onSort = onSort
self.onMainFilter = onMainFilter
self.onMap = onMap
}
// MARK: - View
var body: some View {
HStack(spacing: 0) {
RoundedButton(
systemName: sortIcon,
tintColor: tintColor,
action: {
model.sort.advance()
onSort(model.sort)
}
)
Spacer()
Button {
onMainFilter()
} label: {
HStack {
Image(systemName: "magnifyingglass")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 12)
Text(filterTitle)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay {
Capsule().stroke(AppTheme.Colors.gray.color, lineWidth: 1)
}
}
.font(.caption)
.tint(tintColor)
Spacer()
RoundedButton(
systemName: "globe",
tintColor: tintColor,
action: onMap
)
}
.frame(maxWidth: .infinity)
.background(Color.clear)
}
private var tintColor: Color {
Color.blue
//AppTheme.Colors.lightBlue.color
}
private var filterTitle: String {
switch model.filter {
case .inRadius: "filter_area".localized
case .positionRelevant: "filter_relevant".localized
case .worldWide: "filter_all".localized
case .userFelt: "filter_felt".localized
}
}
private var sortIcon: String {
switch model.sort {
case .time: "clock"
case .position:
if #available(iOS 16, *) {
"compass.drawing"
} else {
"ruler"
}
case .magnitude: "thermometer"
}
}
}
private struct RoundedButton: View {
let systemName: String
let tintColor: Color
let action: () -> Void
var body: some View {
Button {
action()
} label: {
Image(systemName: systemName)
.resizable()
.tint(tintColor)
.aspectRatio(contentMode: .fit)
.frame(maxHeight: .infinity)
.frame(width: 40.0)
.padding(8)
.overlay {
Capsule().stroke(AppTheme.Colors.gray.color, lineWidth: 1)
}
.animation(nil, value: systemName) // previene animazioni implicite nel bottone
}
}
}
#Preview {
SeismicNetworkFilterRecapView(
model: .init(),
onSort: { _ in },
onMainFilter: {},
onMap: {}
)
.frame(height: 34.0)
}
@@ -47,7 +47,7 @@ class SeismicNetworkScrollIndicatorView: UIView {
seismics.enumerated().forEach { index, seismic in
// Disegniamo un rettangolo per ogni sisma, quello evidenziato deve avere un contorno rosso.
// Ci sono situazioni in cui ci sono molti sismi da mostrare, quindi in quel caso facciamo alcune modifiche:
// - usiamo un'altezza minma per il sisma evidenziato
// - usiamo un'altezza minima per il sisma evidenziato
// - per il sisma evidenziato, anche il contenuto è rosso (e non solo il bordo)
// - negli altri sismi, non mostriamo il bordo
@@ -74,9 +74,22 @@ class SeismicNetworkScrollIndicatorView: UIView {
// Dobbiamo eventualmente calcolare un offset aggiuntivo,
// perchè il sisma evidenziato ha un'altezza maggiore (se i rettangoli sono piccoli)
let rectHeight = rectStandardHeight
let offset: CGFloat = (index > highlightIndex && smallRectangles) ? rectHighlightedMinHeight : 0
let yPosition = CGFloat(index) * rectHeight + offset
let rectangle = CGRect(x: 0, y: yPosition, width: rectStandardWidth, height: rectHeight)
var offset: CGFloat = 0
if index > highlightIndex && smallRectangles {
// calcoliamo l'offset prima del rettangolo evidenziato
let preOffset = CGFloat(highlightIndex - 1) * rectStandardHeight
// offset diverso dovuto all'altezza diversa del rettangolo evidenziato
let highlightOffset = rectHighlightedMinHeight
// calcoliamo l'offset tra il rettangolo evidenziato e quello corrente
let postOffset = CGFloat(index - highlightIndex) * rectStandardHeight
offset = preOffset + highlightOffset + postOffset
} else {
// siamo prima del rettangolo evidenziato, non abbiamo calcoli da fare
offset = CGFloat(index) * rectHeight
}
let rectangle = CGRect(x: 0, y: offset, width: rectStandardWidth, height: rectHeight)
let fillColor = seismic.colors.textColor.withAlphaComponent(0.3)
context?.setFillColor(fillColor.cgColor)
@@ -87,8 +100,7 @@ class SeismicNetworkScrollIndicatorView: UIView {
let borderWidth: CGFloat = 0.5
context?.setStrokeColor(AppTheme.Colors.gray.cgColor)
context?.setLineWidth(borderWidth) // Spessore del bordo
context?.stroke(rectangle) // Evita che il bordo venga tagliato
context?.stroke(rectangle)
}
}
}
@@ -8,21 +8,62 @@
import Foundation
struct MagnitudeColors {
let textColor: UIColor
let startColor: UIColor
let endColor: UIColor
}
struct SeismicNetworkMinimalViewModel {
private let seismic: EQNSisma
let place: String
let isPreliminary: Bool
let magnitude: String
let time: String
let distance: String
let smartphones: String
let users: String
let colors: MagnitudeColors
// MARK: - Init
init(seismic: EQNSisma) {
self.seismic = seismic
self.place = seismic.place
let isPreliminary = seismic.preliminary.intValue > 0
self.isPreliminary = isPreliminary
self.magnitude = String(format: "%.1f", seismic.magnitude.doubleValue)
let time = EQNUtility.formattedString(forTimeDifference: Int(seismic.timeDifference))
self.time = time
let distanceRounded = Int(round(seismic.userDistance))
self.distance = "\(distanceRounded) km"
if seismic.smartphoneNumber.intValue > 0 {
self.smartphones = String(format: NSLocalizedString("official_smartphones", comment: ""), seismic.smartphoneNumber)
} else {
self.smartphones = ""
}
if seismic.userNumber.intValue > 0 {
self.users = String(format: NSLocalizedString("official_reports", comment: ""), seismic.userNumber)
} else {
self.users = ""
}
self.colors = calculateColors(for: seismic.magnitude.doubleValue)
}
}
struct SeismicNetworkViewModel {
struct MagnitudeColors {
let textColor: UIColor
let startColor: UIColor
let endColor: UIColor
}
private let seismic: EQNSisma
let place: String
let network: String
let isPreliminary: Bool
let magnitude: String
let rawMagnitude: Double
let depth: String
let time: String
let distance: String
@@ -38,7 +79,6 @@ struct SeismicNetworkViewModel {
self.seismic = seismic
self.place = seismic.place
self.network = seismic.provider
self.rawMagnitude = seismic.magnitude.doubleValue
let isPreliminary = seismic.preliminary.intValue > 0
self.isPreliminary = isPreliminary
@@ -67,7 +107,7 @@ struct SeismicNetworkViewModel {
let coordinateText = EQNUtility.coordinateString(coordinate: seismic.coordinate.coordinate)
self.coordinate = "\(coordinateText)"
let population = Self.formatPopulation(seismic.population100km)
let population = formatPopulation(seismic.population100km)
self.population = String(format: NSLocalizedString("share_radius100", comment: ""), population)
if seismic.smartphoneNumber.intValue > 0 {
@@ -81,78 +121,8 @@ struct SeismicNetworkViewModel {
self.users = ""
}
self.colors = Self.calculateColors(for: seismic.magnitude.doubleValue)
self.colors = calculateColors(for: seismic.magnitude.doubleValue)
}
// MARK: - Private
/// Format population value (ex. 1.5M, 2.4k)
private static func formatPopulation(_ population: Double) -> String {
var populationString = ""
if population > 999_999 {
let roundedPopulation = round(population / 100_000) / 10
populationString = "\(roundedPopulation)M"
} else if population > 999 {
let roundedPopulation = round(population / 100) / 10
populationString = "\(roundedPopulation)K"
} else {
let roundedPopulation = round(population)
populationString = "\(roundedPopulation)"
}
return populationString
}
/// Calculate colors to use for text and background of the cell
private static func calculateColors(for magnitude: Double) -> MagnitudeColors {
var textColor = UIColor.black
var r = 0, g = 0, b = 0
if (magnitude < 2.0) {
let fraction: Double = 1 - (magnitude - 0.0) / (2.0 - 0.0)
r = Int(round(200.0 + (255.0 - 200.0) * fraction))
g = Int(round(226.0 + (255.0 - 226.0) * fraction))
b = Int(round(196.0 + (255.0 - 196.0) * fraction))
textColor = UIColor(red: 12.0 / 255.0, green: 115.0 / 255.0, blue: 160.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 2.0 && magnitude < 3.5) {
let fraction: Double = 1 - (magnitude - 2) / (3.5 - 2)
r = Int(round(136.0 + (200.0 - 136.0) * fraction))
g = Int(round(175.0 + (226.0 - 175.0) * fraction))
b = Int(round(131.0 + (196.0 - 131.0) * fraction))
textColor = UIColor(red: 12.0 / 255.0, green: 160.0 / 255.0, blue: 35.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 3.5 && magnitude < 4.5) {
let fraction: Double = 1 - (magnitude - 3.5) / (4.5 - 3.5)
r = 252
g = Int(round(233.0 + (253.0 - 233.0) * fraction))
b = Int(round(179.0 + (209.0 - 179.0) * fraction))
textColor = UIColor(red: 244.0 / 255.0, green: 195.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 4.5 && magnitude < 5.5) {
let fraction: Double = 1 - (magnitude - 4.5) / (5.5 - 4.5)
r = 252
g = Int(round(159.0 + (197.0 - 159.0) * fraction))
b = Int(round(161.0 + (197.0 - 161.0) * fraction))
textColor = UIColor(red: 255.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 5.5) {
let fraction: Double = 1 - (magnitude - 5.5) / (10 - 5.5)
r = Int(round(190.0 + (254.0 - 190.0) * fraction))
g = Int(round(124.0 + (219.0 - 124.0) * fraction))
b = 255
textColor = UIColor(red: 183.0 / 255.0, green: 60.0 / 255.0, blue: 252.0 / 255.0, alpha: 1.0)
}
let r2 = min(r + 30, 255)
let g2 = min(g + 30, 255)
let b2 = min(b + 30, 255)
let startColor = UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: 1.0)
let endColor = UIColor(red: CGFloat(r2) / 255.0, green: CGFloat(g2) / 255.0, blue: CGFloat(b2) / 255.0, alpha: 1.0)
return .init(textColor: textColor, startColor: startColor, endColor: endColor)
}
}
extension SeismicNetworkViewModel: Equatable {
@@ -160,3 +130,72 @@ extension SeismicNetworkViewModel: Equatable {
return lhs.seismic == rhs.seismic
}
}
// MARK: - Helpers
/// Calculate colors to use for text and background of the cell
private func calculateColors(for magnitude: Double) -> MagnitudeColors {
var textColor = UIColor.black
var r = 0, g = 0, b = 0
if (magnitude < 2.0) {
let fraction: Double = 1 - (magnitude - 0.0) / (2.0 - 0.0)
r = Int(round(200.0 + (255.0 - 200.0) * fraction))
g = Int(round(226.0 + (255.0 - 226.0) * fraction))
b = Int(round(196.0 + (255.0 - 196.0) * fraction))
textColor = UIColor(red: 12.0 / 255.0, green: 115.0 / 255.0, blue: 160.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 2.0 && magnitude < 3.5) {
let fraction: Double = 1 - (magnitude - 2) / (3.5 - 2)
r = Int(round(136.0 + (200.0 - 136.0) * fraction))
g = Int(round(175.0 + (226.0 - 175.0) * fraction))
b = Int(round(131.0 + (196.0 - 131.0) * fraction))
textColor = UIColor(red: 12.0 / 255.0, green: 160.0 / 255.0, blue: 35.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 3.5 && magnitude < 4.5) {
let fraction: Double = 1 - (magnitude - 3.5) / (4.5 - 3.5)
r = 252
g = Int(round(233.0 + (253.0 - 233.0) * fraction))
b = Int(round(179.0 + (209.0 - 179.0) * fraction))
textColor = UIColor(red: 244.0 / 255.0, green: 195.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 4.5 && magnitude < 5.5) {
let fraction: Double = 1 - (magnitude - 4.5) / (5.5 - 4.5)
r = 252
g = Int(round(159.0 + (197.0 - 159.0) * fraction))
b = Int(round(161.0 + (197.0 - 161.0) * fraction))
textColor = UIColor(red: 255.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 5.5) {
let fraction: Double = 1 - (magnitude - 5.5) / (10 - 5.5)
r = Int(round(190.0 + (254.0 - 190.0) * fraction))
g = Int(round(124.0 + (219.0 - 124.0) * fraction))
b = 255
textColor = UIColor(red: 183.0 / 255.0, green: 60.0 / 255.0, blue: 252.0 / 255.0, alpha: 1.0)
}
let r2 = min(r + 30, 255)
let g2 = min(g + 30, 255)
let b2 = min(b + 30, 255)
let startColor = UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: 1.0)
let endColor = UIColor(red: CGFloat(r2) / 255.0, green: CGFloat(g2) / 255.0, blue: CGFloat(b2) / 255.0, alpha: 1.0)
return .init(textColor: textColor, startColor: startColor, endColor: endColor)
}
/// Format population value (ex. 1.5M, 2.4k)
private func formatPopulation(_ population: Double) -> String {
var populationString = ""
if population > 999_999 {
let roundedPopulation = round(population / 100_000) / 10
populationString = "\(roundedPopulation)M"
} else if population > 999 {
let roundedPopulation = round(population / 100) / 10
populationString = "\(roundedPopulation)K"
} else {
let roundedPopulation = round(population)
populationString = "\(roundedPopulation)"
}
return populationString
}
@@ -21,6 +21,30 @@ class SeismicNetworksIntensityMapViewController: EQNBaseMapViewController {
override var isFilterViewVisible: Bool { false }
override var isCloseButtonVisible: Bool { false }
// MARK: - UI
lazy var descriptionView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = AppTheme.Colors.pureBlue
let descriptionLabel = UILabel()
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
descriptionLabel.numberOfLines = 0
descriptionLabel.textColor = .white
descriptionLabel.font = .preferredFont(forTextStyle: .subheadline)
descriptionLabel.textAlignment = .center
descriptionLabel.text = NSLocalizedString("shakemap_description", comment: "")
view.addSubview(descriptionLabel)
descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 2.0).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -2.0).isActive = true
descriptionLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 2.0).isActive = true
descriptionLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -2.0).isActive = true
return view
}()
// MARK: - Init
init(
@@ -39,27 +63,10 @@ class SeismicNetworksIntensityMapViewController: EQNBaseMapViewController {
override func extraUI() {
super.extraUI()
let descriptionView = UIView()
descriptionView.translatesAutoresizingMaskIntoConstraints = false
descriptionView.backgroundColor = AppTheme.Colors.pureBlue
view.addSubview(descriptionView)
descriptionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
descriptionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
descriptionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
let descriptionLabel = UILabel()
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
descriptionLabel.numberOfLines = 0
descriptionLabel.textColor = .white
descriptionLabel.font = .preferredFont(forTextStyle: .subheadline)
descriptionLabel.textAlignment = .center
descriptionLabel.text = NSLocalizedString("shakemap_description", comment: "")
descriptionView.addSubview(descriptionLabel)
descriptionLabel.leadingAnchor.constraint(equalTo: descriptionView.leadingAnchor, constant: 2.0).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: descriptionView.trailingAnchor, constant: -2.0).isActive = true
descriptionLabel.topAnchor.constraint(equalTo: descriptionView.topAnchor, constant: 2.0).isActive = true
descriptionLabel.bottomAnchor.constraint(equalTo: descriptionView.bottomAnchor, constant: -2.0).isActive = true
}
override func configureUI() {
@@ -133,12 +140,16 @@ class SeismicNetworksIntensityMapViewController: EQNBaseMapViewController {
}
private func nextPinStyle() {
pinStyle.next()
pinStyle.advance()
reloadMap()
}
private func shareScreenshot() {
let screenshot = createSnapshot()
let screenshot = createSnapshot(prepare: {
descriptionView.isHidden = true
}, restore: {
descriptionView.isHidden = false
})
let controller = UIActivityViewController(activityItems: [screenshot], applicationActivities: [])
present(controller, animated: true)
@@ -199,7 +210,11 @@ class SeismicNetworksIntensityMapViewController: EQNBaseMapViewController {
let minIntensity = shakemaps.map { $0.intensity }.min() ?? 0
let maxIntensity = shakemaps.map { $0.intensity }.max() ?? 255
let indexColor = Int(round((intensity-minIntensity)/(maxIntensity-minIntensity)*255))
let indexColor = if minIntensity == maxIntensity {
0
} else {
Int(round((intensity-minIntensity)/(maxIntensity-minIntensity)*255))
}
let lineColor = UIColor(hexString: shakemapColors[indexColor]) ?? .white
let textColor: UIColor = indexColor < 65 ? .white : .black
@@ -218,7 +233,7 @@ extension EQNMapAnnotationShakemap {
case .full, .light:
let identifier = EQNSeismicAnnotationView.identifier(for: style)
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier, for: self) as! EQNSeismicAnnotationView
annotationView.magnitude = String(format: "%.1f", shakemap.intensity)
annotationView.magnitude = intensityString(from: shakemap.intensity)
annotationView.magnitudeTextColor = intensityTextColor ?? .black
annotationView.magnitudeBackgroundColor = intensityColor
annotationView.canShowCallout = true
@@ -227,6 +242,26 @@ extension EQNMapAnnotationShakemap {
return nil
}
}
private func intensityString(from intensity: Float) -> String {
let intensityRounded = (intensity * 10).rounded() / 10
let intensityFloor = floor(intensityRounded)
let romanNumerals: [Int: String] = [
1: "I", 2: "II", 3: "III", 4: "IV",
5: "V", 6: "VI", 7: "VII", 8: "VIII",
9: "IX", 10: "X", 11: "XI", 12: "XII"
]
var result = romanNumerals[Int(intensityFloor)] ?? ""
if intensityRounded != intensityFloor {
let reminder = Int(((intensityRounded - intensityFloor) * 10).rounded())
result += ".\(reminder)"
}
return result
}
}
fileprivate class ShakemapPolyline: MKPolyline {
@@ -63,14 +63,17 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
// MARK: - Internal
private let seismic: EQNSisma
private let seismic: EQNSisma?
private var allSeismics: [EQNSisma]
/// Contains circles drawed on the map
private var mapCircles = [MKCircle]()
// MARK: - Init
init(seismic: EQNSisma, allSeismics: [EQNSisma]) {
init(
seismic: EQNSisma?,
allSeismics: [EQNSisma]
) {
self.seismic = seismic
self.allSeismics = allSeismics
super.init()
@@ -128,7 +131,11 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
}
override func elaborateMapCenter() {
setMapCenter(for: seismic.coordinate)
if let seismic {
setMapCenter(for: seismic.coordinate)
} else if let location = CLLocationManager().location {
setMapCenter(for: location)
}
}
override func didTapAnnotation(_ annotation: MKAnnotation) {
@@ -178,7 +185,7 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
// MARK: - Private
private func nextPinStyle() {
pinStyle.next()
pinStyle.advance()
reloadMap()
}
@@ -7,6 +7,7 @@
//
import UIKit
import SwiftUI
import EventKitUI
import DZNEmptyDataSet
import Shogun
@@ -18,6 +19,12 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
case advertise(NativeAd)
}
enum CardDisplayType: Int, CaseIterable {
case small
case full
case minimal
}
private static let SegueIdentifierFilters = "ShowFilters"
private static let SegueIdentifierCardSettings = "ShowCardSettings"
@@ -34,9 +41,17 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
/// Cells to display (must be seismics or ad banners)
private var rows = [CellType]()
/// Type of cards to show
private var cardDisplayType: CardDisplayType {
get { AppPreferences.shared.seismicNetworksCardStyle }
set { AppPreferences.shared.seismicNetworksCardStyle = newValue }
}
private var seismicViewModels = [SeismicNetworkViewModel]()
/// Informations to display on a single cell
private var informations = [SeismicNetworkTableViewCell.InformationType]()
private var informations: [SeismicNetworkTableViewCell.InformationType] {
get { AppPreferences.shared.seismicNetworksInformations }
set { AppPreferences.shared.seismicNetworksInformations = newValue }
}
/// Index path of row with map expanded
private var openMapIndexPath: IndexPath?
/// Push notification opened by the user
@@ -53,8 +68,7 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
// MARK: - UI
@IBOutlet private weak var expandeCollapseButton: UIBarButtonItem!
@IBOutlet private weak var sortButton: UIBarButtonItem!
@IBOutlet private weak var displayModeButton: UIBarButtonItem!
private var tableViewTopConstraint: NSLayoutConstraint?
private lazy var tableView: UITableView = {
@@ -97,6 +111,32 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
view.layer.borderColor = AppTheme.Colors.gray.cgColor
return view
}()
// model is needed in order to send data to SwiftUI view
private let model = SeismicNetworkFilterRecapView.Model()
private lazy var filterRecapView: UIView = {
let hosting = UIHostingController(
rootView: SeismicNetworkFilterRecapView(
model: model,
onSort: { [weak self] sort in
self?.changeSort(to: sort)
},
onMainFilter: { [weak self] in
self?.openFilter()
},
onMap: { [weak self] in
self?.showMapDetail(for: nil)
}
)
)
addChild(hosting)
let filterRecapView = hosting.view!
hosting.view.isOpaque = false
hosting.view.translatesAutoresizingMaskIntoConstraints = false
hosting.didMove(toParent: self)
return hosting.view
}()
// MARK: - View Lifecycle
@@ -133,19 +173,44 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
// MARK: - Private
private func setupUI() {
view.backgroundColor = tableView.backgroundColor
view.addSubview(scrollIndicatorView)
view.addSubview(tableView)
view.addSubview(filterRecapView)
scrollIndicatorView.topAnchor.constraint(equalTo: tableView.topAnchor).isActive = true
scrollIndicatorView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollIndicatorView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
scrollIndicatorView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor).isActive = true
scrollIndicatorView.widthAnchor.constraint(equalToConstant: 10.0).isActive = true
tableViewTopConstraint = tableView.topAnchor.constraint(equalTo: view.topAnchor)
tableViewTopConstraint?.isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: scrollIndicatorView.leadingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.bottomAnchor
.constraint(
equalTo: filterRecapView.topAnchor,
constant: -8
).isActive = true
filterRecapView.heightAnchor.constraint(equalToConstant: 34.0).isActive = true
filterRecapView.leadingAnchor
.constraint(
equalTo: view.leadingAnchor,
constant: 10.0
).isActive = true
filterRecapView.trailingAnchor
.constraint(
equalTo: view.trailingAnchor,
constant: -10.0
).isActive = true
filterRecapView.topAnchor
.constraint(equalTo: tableView.bottomAnchor).isActive = true
filterRecapView.bottomAnchor
.constraint(
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -8
).isActive = true
}
private func setupFilterView(isVisible: Bool) {
@@ -182,34 +247,18 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
tableView.estimatedRowHeight = 300.0
tableView.rowHeight = UITableView.automaticDimension
tableView.registerCell(for: SeismicNetworkTableViewCell.self)
tableView.registerCell(for: SeismicNetworkMinimalTableViewCell.self)
tableView.registerCell(for: SeismicNetworkAdvertiseTableViewCell.self)
tableView.emptyDataSetSource = self
tableView.separatorStyle = .none
setupSortMenu()
}
private func setupSortMenu() {
let currentSort = EQNSeismic.shared.sort
sortButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
UIAction(title: NSLocalizedString("sort_date", comment: ""), image: UIImage(systemName: "calendar"), state: currentSort == .time ? .on : .off) { [weak self ] _ in
self?.changeSort(to: .time)
},
UIAction(title: NSLocalizedString("sort_position", comment: ""), image: UIImage(systemName: "ruler"), state: currentSort == .position ? .on : .off) { [weak self] _ in
self?.changeSort(to: .position)
},
UIAction(title: NSLocalizedString("sort_magnitude", comment: ""), image: UIImage(systemName: "thermometer"), state: currentSort == .magnitude ? .on : .off) { [weak self] _ in
self?.changeSort(to: .magnitude)
}
])
tableView.contentInset = EQNBaseContainerTableViewCell.EdgeInsets
}
private func checkForLocation() {
// check if a valid location is available,
// otherwise change the filter settings
if !isLocationAvailable() {
EQNSeismic.shared.filterOption = .worldWide
EQNSeismic.shared.saveFilters()
updateFilter(type: .worldWide)
}
}
@@ -228,7 +277,7 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
}
}
private func showMapDetail(for seismic: EQNSisma) {
private func showMapDetail(for seismic: EQNSisma?) {
let seismics = getSeismics()
let controller = SeismicNetworksMapDetailViewController(seismic: seismic, allSeismics: seismics)
controller.delegate = self
@@ -259,14 +308,13 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
private func refreshUI() {
elaborateData()
if let saved = UserDefaults.standard.array(forKey: EQNUserDefaultKeySesmicInformations) as? [Int] {
informations = saved.compactMap { SeismicNetworkTableViewCell.InformationType(rawValue: $0) }
}
if informations.contains(.buttons) {
expandeCollapseButton.image = UIImage(named: "navbar-icon-arrow-collapse")
} else {
expandeCollapseButton.image = UIImage(named: "navbar-icon-arrow-expand")
switch cardDisplayType {
case .small:
displayModeButton.image = UIImage(systemName: "1.square")
case .full:
displayModeButton.image = UIImage(systemName: "2.square")
case .minimal:
displayModeButton.image = UIImage(systemName: "3.square")
}
tableView.reloadData()
@@ -320,10 +368,7 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
}
private func changeSort(to sort: EQNSeismic.Sort) {
EQNSeismic.shared.sort = sort
EQNSeismic.shared.saveFilters()
setupSortMenu()
updateFilter(sort: sort)
refreshUI()
}
@@ -553,11 +598,22 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
private func updateFilter(
type: EQNSeismic.FilterType? = nil,
sort: EQNSeismic.Sort? = nil,
radius: Double? = nil,
magnitude: Double? = nil
) {
if let type {
let previous = EQNSeismic.shared.filterOption
if previous == .userFelt && previous != type {
loadData(forced: true)
}
EQNSeismic.shared.filterOption = type
model.filter = type
}
if let sort {
EQNSeismic.shared.sort = sort
model.sort = sort
}
if let radius {
EQNSeismic.shared.maximumDistance = String(format: "%.0f", radius)
@@ -613,9 +669,15 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
if let centerIndexPath = getCenterCellIndexPath(), centerIndexPath != currentCenteredIndexPath {
currentCenteredIndexPath = centerIndexPath
let row = rows[centerIndexPath.row]
if case .seismic = row, seismicViewModels.count > centerIndexPath.row {
scrollIndicatorView.highlighted = seismicViewModels[centerIndexPath.row]
if rows.count > centerIndexPath.row {
let row = rows[centerIndexPath.row]
if case .seismic = row, seismicViewModels.count > centerIndexPath.row {
scrollIndicatorView.highlighted = seismicViewModels[centerIndexPath.row]
} else {
scrollIndicatorView.highlighted = nil
}
} else {
scrollIndicatorView.highlighted = nil
}
}
}
@@ -626,18 +688,22 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
loadData(forced: true)
}
@IBAction func openFilterTapped(_ sender: Any) {
private func openFilter() {
performSegue(withIdentifier: Self.SegueIdentifierFilters, sender: nil)
}
@IBAction func collapseExpandTapped(_ sender: Any) {
if informations.contains(.buttons) {
cardDisplayType.advance()
switch cardDisplayType {
case .small:
informations.removeAll(where: { $0 == .buttons })
} else {
case .full:
informations.append(.buttons)
case .minimal:
break
}
UserDefaults.standard.set(informations.map { $0.rawValue }, forKey: EQNUserDefaultKeySesmicInformations)
refreshUI()
}
@@ -651,17 +717,26 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
let row = rows[indexPath.row]
switch row {
case .seismic(let seismic):
let cell = tableView.dequeueReusableCell(cellIdentifiable: SeismicNetworkTableViewCell.self, for: indexPath)
var type = SeismicNetworkTableViewCell.DisplayType.normal
if openMapIndexPath == indexPath {
type = .mapExpanded
switch cardDisplayType {
case .small, .full:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SeismicNetworkTableViewCell.self, for: indexPath)
var type = SeismicNetworkTableViewCell.DisplayType.normal
if openMapIndexPath == indexPath {
type = .mapExpanded
}
let isPushSelected = isSeismicToHighlight(seismic: seismic)
cell.configure(with: seismic, type: type, informations: informations, isPushSelected: isPushSelected)
cell.delegate = self
return cell
case .minimal:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SeismicNetworkMinimalTableViewCell.self, for: indexPath)
let isPushSelected = isSeismicToHighlight(seismic: seismic)
cell.configure(with: seismic, isPushSelected: isPushSelected)
cell.delegate = self
return cell
}
let isPushSelected = isSeismicToHighlight(seismic: seismic)
cell.configure(with: seismic, type: type, informations: informations, isPushSelected: isPushSelected)
cell.delegate = self
return cell
case .advertise(let nativeAd):
let cell = tableView.dequeueReusableCell(cellIdentifiable: SeismicNetworkAdvertiseTableViewCell.self, for: indexPath)
cell.loadNativeAd(nativeAd)
@@ -754,9 +829,9 @@ extension SeismicNetworksViewController: NativeAdLoaderDelegate {
}
}
extension SeismicNetworksViewController: SeismicNetworkTableViewCellDelegate {
extension SeismicNetworksViewController: SeismicNetworkBaseTableViewCellDelegate {
func seismicNetworkCellDidTapShare(_ cell: SeismicNetworkTableViewCell) {
func seismicNetworkCellDidTapShare(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
// create a snapshot of the cell and share with default share sheet
@@ -773,7 +848,7 @@ extension SeismicNetworksViewController: SeismicNetworkTableViewCellDelegate {
present(controller, animated: true)
}
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkTableViewCell) {
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell) else { return }
let indexToReloads = [openMapIndexPath, index].compactMap { $0 }
@@ -782,29 +857,29 @@ extension SeismicNetworksViewController: SeismicNetworkTableViewCellDelegate {
tableView.reloadRows(at: indexToReloads, with: .automatic)
}
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkTableViewCell) {
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
showMapDetail(for: seismic)
}
func seismicNetworkCellDidTapIntensityMapDetail(_ cell: SeismicNetworkTableViewCell) {
func seismicNetworkCellDidTapIntensityMapDetail(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
showIntensityMap(for: seismic)
}
func seismicNetworkCellDidTapCalendar(_ cell: SeismicNetworkTableViewCell) {
func seismicNetworkCellDidTapCalendar(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
openCalendar(for: seismic)
}
func seismicNetworkCellDidTapSettings(_ cell: SeismicNetworkTableViewCell) {
func seismicNetworkCellDidTapSettings(_ cell: SeismicNetworkBaseTableViewCell) {
performSegue(withIdentifier: Self.SegueIdentifierCardSettings, sender: nil)
}
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkTableViewCell) {
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell) else { return }
let indexToReloads = [openMapIndexPath, index].compactMap { $0 }
@@ -823,6 +898,7 @@ extension SeismicNetworksViewController: EKEventEditViewDelegate {
extension SeismicNetworksViewController: SeismicFiltersViewControllerDelegate {
func seismicFiltersControllerDidUpdateFilters(_ controller: SeismicFiltersViewController) {
model.filter = controller.currentFilterType
loadData(forced: controller.needsDataUpdate)
refreshUI()
}
@@ -13,14 +13,17 @@ class SettingsRealTimeAlertsViewController: SettingsBaseTableViewController {
private enum RowIdentifier: Int {
case abilitaNotifiche
case disabilitaSuonoAllerta
case abilitaCriticalAlerts
}
private var isNotificationEnabled = false
private var isMildQuakeSoundDisabled = false
private var isCriticalAlertsEnabled = false
private let settings: [SettingItem] = [
.init(type: .enable, title: NSLocalizedString("options_notification_enable_alarm", comment: "")),
.init(type: .enable, title: NSLocalizedString("options_notification_disable_sound", comment: "")),
.init(type: .enable, title: NSLocalizedString("critical_alerts_setting", comment: ""))
]
@@ -56,6 +59,7 @@ class SettingsRealTimeAlertsViewController: SettingsBaseTableViewController {
let saved = EQNSettingRealTimeAlert.shared
isNotificationEnabled = saved.isAbilitato
isMildQuakeSoundDisabled = saved.isMildQuakeSoundDisabled
isCriticalAlertsEnabled = saved.isCriticalAlertsEnabled
}
@@ -93,6 +97,11 @@ class SettingsRealTimeAlertsViewController: SettingsBaseTableViewController {
cell.valueChanged = { [weak self] enabled in
self?.onChangeNotificationEnabled(enabled)
}
case .disabilitaSuonoAllerta:
cell.toggleSwitch.isOn = isMildQuakeSoundDisabled
cell.valueChanged = { [weak self] enabled in
self?.onChangeDisableSoundEnabled(enabled)
}
case .abilitaCriticalAlerts:
cell.toggleSwitch.isOn = isCriticalAlertsEnabled
cell.valueChanged = { [weak self] enabled in
@@ -116,6 +125,14 @@ class SettingsRealTimeAlertsViewController: SettingsBaseTableViewController {
tableView.reloadData()
}
private func onChangeDisableSoundEnabled(_ enabled: Bool) {
isMildQuakeSoundDisabled = enabled
EQNSettingRealTimeAlert.shared.isMildQuakeSoundDisabled = isMildQuakeSoundDisabled
EQNSettingRealTimeAlert.shared.saveUserInfo()
tableView.reloadData()
}
private func onChangeCriticalAlertsEnabled(_ enabled: Bool) {
if enabled {
askForCriticalAlertsPermission()
+1 -3
View File
@@ -49,7 +49,7 @@ static NSString * const EQNServerUrlCalibration = @"https://srv.earthquakenetwor
// download rete smartphone
static NSString * const EQNServerUrlDownloadSmartphoneNetwork = @"https://cache.earthquakenetwork.it/distquake_count_redis3.php";
// download area check
static NSString * const EQNServerUrlDownloadAreaCheck = @"https://srv.earthquakenetwork.it/distquake_download_areacheck.php";
static NSString * const EQNServerUrlDownloadAreaCheck = @"https://cache.earthquakenetwork.it/distquake_download_areacheck.php";
// download pastquakes
static NSString * const EQNServerUrlDownloadPastQuakes = @"https://srv.earthquakenetwork.it/distquake_download_pastquakes.php";
// download segnalazioni
@@ -64,8 +64,6 @@ static NSString * const EQNServerUrlAlertSimulator = @"https://srv.earthquakenet
#pragma mark - UserDefaults Keys
static NSString * const EQNUserDefaultKeySesmicInformations = @"EQNetwork.SeismicInformations";
static NSString * const EQNUserDefaultKeyOneShotShowCountry = @"EQNetwork.OneShot.CountrySelection";
static NSString * const EQNUserDefaultSeismicNetworkCards = @"EQNData.RetiSismiche";
#pragma mark - NSNotification
@@ -84,7 +84,6 @@ public class Log {
shared.log(level: .debug, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName)
}
@available(iOS 15.0, *)
public func dumpLog() async -> String {
return (try? await getLogEntries()) ?? ""
}
@@ -104,7 +103,6 @@ public class Log {
/// Retrieve log entries from a specified time.
/// - Returns: String of log entries, newlines separated
@available(iOS 15.0, *)
private func getLogEntries() async throws -> String {
let logTask = Task.init(priority: .utility) { () -> String in
let logs = try retrieveLogEntries()
@@ -116,7 +114,6 @@ public class Log {
return try await logTask.value
}
@available(iOS 15.0, *)
private func retrieveLogEntries() throws -> [OSLogEntryLog] {
// Open the log store.
let logStore = try OSLogStore(scope: .currentProcessIdentifier)
@@ -134,7 +131,6 @@ public class Log {
}
}
@available(iOS 15.0, *)
extension OSLogEntryLog.Level: @retroactive CustomStringConvertible {
public var description: String {
switch self {
@@ -39,4 +39,27 @@ class AppPreferences: NSObject {
UserDefaults.standard.set(newValue.rawValue, forKey: UserDefaults.MapPinStyle)
}
}
var seismicNetworksInformations: [SeismicNetworkTableViewCell.InformationType] {
get {
if let saved = UserDefaults.standard.array(forKey: UserDefaults.SeismicNetworksCardInformations) as? [Int] {
let informations = saved.compactMap { SeismicNetworkTableViewCell.InformationType(rawValue: $0) }
return informations
}
return [.buttons, .distance, .coordinate, .population, .intensityMap]
}
set {
UserDefaults.standard.set(newValue.map { $0.rawValue }, forKey: UserDefaults.SeismicNetworksCardInformations)
}
}
var seismicNetworksCardStyle: SeismicNetworksViewController.CardDisplayType {
get {
let saved = UserDefaults.standard.integer(forKey: UserDefaults.SeismicNetworksCardStyle)
return SeismicNetworksViewController.CardDisplayType(rawValue: saved) ?? .small
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: UserDefaults.SeismicNetworksCardStyle)
}
}
}
@@ -17,25 +17,15 @@ public class EQNUserDefaultsCommand: EQNCommandProtocol {
func execute() {
print("[EQNUserDefaultsCommand] Start execute")
applyDefaultSettings()
migrationV5_8()
migrationFirstAppStat()
migrationCriticalAlerts()
migrationV5_9()
migrationV5_10()
}
// MARK: - Private
private func applyDefaultSettings() {
// seismic card settings
if UserDefaults.standard.array(forKey: EQNUserDefaultKeySesmicInformations) == nil {
let informations: [SeismicNetworkTableViewCell.InformationType] = [.buttons, .distance, .coordinate, .population, .intensityMap]
UserDefaults.standard.set(informations.map { $0.rawValue }, forKey: EQNUserDefaultKeySesmicInformations)
}
}
private func migrationV5_8() {
let migrationPerformed = UserDefaults.standard.bool(forKey: UserDefaults.AppMigrationV5_8)
if migrationPerformed {
@@ -117,15 +107,32 @@ public class EQNUserDefaultsCommand: EQNCommandProtocol {
return
}
if let saved = userDefaults.array(forKey: EQNUserDefaultKeySesmicInformations) as? [Int] {
var informations = saved.compactMap { SeismicNetworkTableViewCell.InformationType(rawValue: $0) }
if !informations.contains(.intensityMap) {
informations.append(.intensityMap)
print("[EQNUserDefaultsCommand] Add intensityMap to seismic informations")
}
userDefaults.set(informations.map { $0.rawValue }, forKey: EQNUserDefaultKeySesmicInformations)
// add new intensity map
var informations = AppPreferences.shared.seismicNetworksInformations
if !informations.contains(.intensityMap) {
informations.append(.intensityMap)
print("[EQNUserDefaultsCommand] Add intensityMap to seismic informations")
}
AppPreferences.shared.seismicNetworksInformations = informations
let cardDisplayType: SeismicNetworksViewController.CardDisplayType = informations.contains(.buttons) ? .full : .small
AppPreferences.shared.seismicNetworksCardStyle = cardDisplayType
userDefaults.set(true, forKey: UserDefaults.AppMigrationV5_9)
}
private func migrationV5_10() {
let userDefaults = UserDefaults.standard
let migrationPerformed = userDefaults.bool(forKey: UserDefaults.AppMigrationV5_10)
if migrationPerformed {
print("[EQNUserDefaultsCommand] Migration v5.10 already performed")
return
}
print("[EQNUserDefaultsCommand] Save default value for mildQuakeSoundDisabled")
EQNSettingRealTimeAlert.shared.isMildQuakeSoundDisabled = true
EQNSettingRealTimeAlert.shared.saveUserInfo()
userDefaults.set(true, forKey: UserDefaults.AppMigrationV5_10)
}
}
+18 -5
View File
@@ -61,8 +61,14 @@
- (void)scaricaDatiReteSmartphone
{
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:[NSURL URLWithString:EQNServerUrlDownloadSmartphoneNetwork] richiesta:EQNTipoChiamataDownloadDati success:^(id result) {
self.rete_smartphone = [[EQNReteSmartphone alloc] initWithInfo:result];
// Parsiamo la risposta (assicurandoci che non contenga valori nulli)
EQNReteSmartphone *rete = [EQNReteSmartphone fromResponse:result];
if (rete != nil) {
self.rete_smartphone = rete;
} else {
NSLog(@"[EQNManager] Impossibile parsare la risposta di DownloadSmartphoneNetwork");
}
[self performSelectorOnMainThread:@selector(scaricaAreaCheck) withObject:nil waitUntilDone:YES];
} failure:^(NSError * error) {
@@ -72,7 +78,12 @@
- (void)scaricaAreaCheck
{
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@?lat=%f&lon=%f", EQNServerUrlDownloadAreaCheck, [EQNUser defaultUser].lastPosition.coordinate.latitude, [EQNUser defaultUser].lastPosition.coordinate.longitude]] richiesta:EQNTipoChiamataAreaCheck success:^(id result) {
// Quantizziamo le coordinate, in modo che venga sfruttata la cache lato server
CLLocation *lastPosition = [EQNUser defaultUser].lastPosition;
double latitude = round(lastPosition.coordinate.latitude * 5.0) / 5.0;
double longitude = round(lastPosition.coordinate.longitude * 5.0) / 5.0;
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@?lat=%f&lon=%f", EQNServerUrlDownloadAreaCheck, latitude, longitude]] richiesta:EQNTipoChiamataAreaCheck success:^(id result) {
self.area_check = [[EQNAreaCheck alloc] initWithInfo:result];
@@ -123,11 +134,13 @@
// L'endpoint per lo scaricamento dei dati prende due parametri:
// - `pro` per il provider selezionato,
// - `mag` per la magnitudo minima.
// Dalla v5.8 non esiste più la selezione delle reti, quindi passiamo sempre "ALL".
// Dalla v5.8 non esiste più la selezione delle reti, quindi il provider da passare diventa:
// - `FELT` se filtro selezionato è sismi percepiti
// - `ALL` in tutti gli altri casi
// Per la magnitudo minima, invece, passiamo 0 per i filtri "raggio" e "rilevanti,
// altrimenti passiamo 2 per il filtro "mondo"
NSString *filterProvider = @"ALL";
NSString *filterProvider = [EQNSeismic shared].filterOption == FilterTypeUserFelt ? @"FELT" : @"ALL";
NSString *filterMagnitude = [EQNSeismic shared].filterOption == FilterTypeWorldWide ? @"2.0" : @"0.0";
NSString *queryString = [NSString stringWithFormat:@"?pro=%@&mag=%@", filterProvider, filterMagnitude];
@@ -20,6 +20,7 @@ class EQNRealtimePushNotification: NSObject, Codable {
private enum CodingKeys: String, CodingKey {
case type
case intensity
case magnitude
case latitude
case longitude
case counter
@@ -36,6 +37,7 @@ class EQNRealtimePushNotification: NSObject, Codable {
let type: String
/// Earthquake intensity
let intensity: Int
let magnitude: Int
/// Earthquake coordinate
let latitude: Double
let longitude: Double
@@ -63,6 +65,7 @@ class EQNRealtimePushNotification: NSObject, Codable {
init(
type: String,
intensity: Int,
magnitude: Int,
latitude: Double,
longitude: Double,
counter: Int,
@@ -76,6 +79,7 @@ class EQNRealtimePushNotification: NSObject, Codable {
) {
self.type = type
self.intensity = intensity
self.magnitude = magnitude
self.latitude = latitude
self.longitude = longitude
self.counter = counter
@@ -201,6 +205,27 @@ class EQNRealtimePushNotification: NSObject, Codable {
return nil
}
var title: String = ""
if let titleKey = alert["loc-key"] as? String, let args = alert["loc-args"] as? [String], let arg = args.first {
title = String(format: NSLocalizedString(titleKey, comment: ""), arg)
}
let displayTitle = payload.string(forKey: "title", orDefault: "")
let displayBody = payload.string(forKey: "body", orDefault: "")
return from(
userInfo: userInfo,
title: title,
displayTitle: displayTitle,
displayBody: displayBody
)
}
static func from(
userInfo: [AnyHashable: Any],
title: String,
displayTitle: String,
displayBody: String
) -> EQNRealtimePushNotification? {
guard let latitude = userInfo.double(forKey: "latitude"),
let longitude = userInfo.double(forKey: "longitude") else {
print("[EQNRealtimePushNotification] Unable to get coordinate from push notification")
@@ -209,6 +234,7 @@ class EQNRealtimePushNotification: NSObject, Codable {
let type = userInfo.string(forKey: "type", orDefault: "")
let intensity = userInfo.integer(forKey: "intensity", orDefault: 0)
let magnitude = userInfo.integer(forKey: "magnitude", orDefault: 0)
let counter = userInfo.integer(forKey: "counter", orDefault: 0)
var dateTime: Date?
@@ -222,16 +248,10 @@ class EQNRealtimePushNotification: NSObject, Codable {
}
let peak = userInfo.double(forKey: "peak")
var title: String = ""
if let titleKey = alert["loc-key"] as? String, let args = alert["loc-args"] as? [String], let arg = args.first {
title = String(format: NSLocalizedString(titleKey, comment: ""), arg)
}
let displayTitle = payload.string(forKey: "title", orDefault: "")
let displayBody = payload.string(forKey: "body", orDefault: "")
return .init(
type: type,
intensity: intensity,
magnitude: magnitude,
latitude: latitude,
longitude: longitude,
counter: counter,
@@ -21,13 +21,27 @@ class EQNReteSmartphone: NSObject {
let top10kAvailable: Int
let top100kAvailable: Int
// MARK: - Class
// Sometimes the response returns a broken response, with all values to null.
// In order to avoid crashes due to ObjC-Swift bridging, we need to take a generic nullable object,
// and then cast to the expected type (a dictionary).
@objc static func from(response: Any?) -> EQNReteSmartphone? {
if let info = response as? [[String: Any]?] {
return .init(info: info)
}
return nil
}
// MARK: - Init
@objc init(info: [[String: Any]]) {
private init(info: [[String: Any]?]) {
// merge array in a single dictionary
let allValues = info.reduce([:]) { (result, dictionary) -> [String: Any] in
return result.merging(dictionary, uniquingKeysWith: { (_, new) in new })
}
let allValues = info
.compactMap { $0 }
.reduce([:]) { (result, dictionary) -> [String: Any] in
return result.merging(dictionary, uniquingKeysWith: { (_, new) in new })
}
self.counterLastDayAlerts = allValues.integer(forKey: "eq", orDefault: 0)
self.counterTotalAlerts = allValues.integer(forKey: "eq_p", orDefault: 0)
@@ -18,7 +18,7 @@ import Foundation
case userFelt
}
enum Sort: Int {
enum Sort: Int, CaseIterable {
case time
case position
case magnitude
@@ -23,6 +23,13 @@ import CoreLocation
return !firstAppStartExecuted
}
// MARK: - Permissions
@objc
var locationAuthorizationStatus: CLAuthorizationStatus {
CLLocationManager().authorizationStatus
}
// MARK: - Firebase Token
@objc
@@ -12,12 +12,4 @@ enum MapPinStyle: Int, CaseIterable {
case circle
case light
case full
mutating func next() {
let all = Self.allCases
let idx = all.firstIndex(of: self)!
let next = all.index(after: idx)
let newValue = all[next == all.endIndex ? all.startIndex : next]
self = newValue
}
}
@@ -16,11 +16,12 @@ class EQNSettingRealTimeAlert: NSObject {
static let shared = EQNSettingRealTimeAlert()
@objc var isAbilitato: Bool
@objc var isCriticalAlertsEnabled: Bool
var isMildQuakeSoundDisabled: Bool
var isCriticalAlertsEnabled: Bool
@objc var sismiDaNotificare: String
var sismiDaNotificare: String
@objc var raggioSismiLievi: String
@objc var raggioSismiForti: String
var raggioSismiForti: String
private static let DefaultSismiDaNotificare = "0"
private static let DefaultRaggioSismiLievi = "250"
@@ -38,6 +39,7 @@ class EQNSettingRealTimeAlert: NSObject {
let sharedDefaults = UserDefaults.appGroup
isCriticalAlertsEnabled = sharedDefaults?.bool(forKey: UserDefaults.AllertaSismicaCriticalAlerts) ?? false
isMildQuakeSoundDisabled = sharedDefaults?.bool(forKey: UserDefaults.AllertaSismicaSuonoDisabilitatoSismaDebole) ?? true
}
// MARK: - Public
@@ -50,12 +52,14 @@ class EQNSettingRealTimeAlert: NSObject {
defaults.set(raggioSismiForti, forKey: UserDefaults.AllertaSismicaRaggioSismiForti)
if let sharedDefaults = UserDefaults.appGroup {
sharedDefaults.set(isCriticalAlertsEnabled, forKey: UserDefaults.AllertaSismicaCriticalAlerts)
sharedDefaults.set(isMildQuakeSoundDisabled, forKey: UserDefaults.AllertaSismicaSuonoDisabilitatoSismaDebole)
}
}
@objc class func saveDefaultValues() {
shared.isAbilitato = true
shared.isCriticalAlertsEnabled = false
shared.isMildQuakeSoundDisabled = true
shared.sismiDaNotificare = Self.DefaultSismiDaNotificare
shared.raggioSismiLievi = Self.DefaultRaggioSismiLievi
shared.raggioSismiForti = Self.DefaultRaggioSismiForti
@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "twitter_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "xcorp_icon.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1 @@
<svg id="vector" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><path fill="#000000" d="M321.8,373.1h36.6L190,137.5H153.4ZM391,389.9H310.6L237,285.1 144.8,389.9H121L226.4,270 121,120h80.4l69.7,99.2L358.4,120h23.8L281.7,234.3Z" id="path_0"/></svg>

After

Width:  |  Height:  |  Size: 281 B

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="CWo-PE-Dqp">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="CWo-PE-Dqp">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -21,27 +21,20 @@
</view>
<navigationItem key="navigationItem" id="6kD-jk-5Aw">
<rightBarButtonItems>
<barButtonItem image="1.square" catalog="system" id="HTN-07-s5p">
<connections>
<action selector="collapseExpandTapped:" destination="tVM-DH-fmv" id="EnD-92-5ZX"/>
</connections>
</barButtonItem>
<barButtonItem image="navbar-icon-refresh" id="ZJh-jF-ILm">
<connections>
<action selector="refreshDataTapped:" destination="tVM-DH-fmv" id="qs5-jS-0Op"/>
</connections>
</barButtonItem>
<barButtonItem image="navbar-icon-filters" id="vOM-Np-CIk">
<connections>
<action selector="openFilterTapped:" destination="tVM-DH-fmv" id="76a-Bl-bCj"/>
</connections>
</barButtonItem>
<barButtonItem image="navbar-icon-sort" id="LyU-KI-3Mb"/>
<barButtonItem image="navbar-icon-arrow-collapse" id="HTN-07-s5p">
<connections>
<action selector="collapseExpandTapped:" destination="tVM-DH-fmv" id="EnD-92-5ZX"/>
</connections>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<outlet property="expandeCollapseButton" destination="HTN-07-s5p" id="lxP-Im-NME"/>
<outlet property="sortButton" destination="LyU-KI-3Mb" id="969-Zg-YBB"/>
<outlet property="displayModeButton" destination="HTN-07-s5p" id="Lhc-Od-MvL"/>
<segue destination="6LP-zk-O1z" kind="presentation" identifier="ShowFilters" modalPresentationStyle="overCurrentContext" modalTransitionStyle="crossDissolve" id="Nzu-iH-UgB"/>
<segue destination="Rfp-kt-2Kx" kind="presentation" identifier="ShowCardSettings" modalPresentationStyle="overCurrentContext" modalTransitionStyle="crossDissolve" id="VWw-16-xGw"/>
</connections>
@@ -59,7 +52,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="jl5-sK-UaA">
<rect key="frame" x="0.0" y="144" width="414" height="614"/>
<rect key="frame" x="0.0" y="192" width="414" height="532"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<outlet property="dataSource" destination="fbo-Ug-IiE" id="meg-jS-D2z"/>
@@ -67,7 +60,7 @@
</connections>
</tableView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="4mK-no-RDk">
<rect key="frame" x="0.0" y="758" width="414" height="55"/>
<rect key="frame" x="0.0" y="724" width="414" height="55"/>
<color key="backgroundColor" systemColor="groupTableViewBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="55" id="1Dm-ex-6od"/>
@@ -276,17 +269,17 @@
<objects>
<viewController storyboardIdentifier="EQNLogViewController" id="noK-2F-IZE" customClass="EQNLogViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="6P7-SV-yrd">
<rect key="frame" x="0.0" y="0.0" width="414" height="886"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="838"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="ija-iK-2hE">
<rect key="frame" x="0.0" y="20" width="414" height="808"/>
<rect key="frame" x="0.0" y="20" width="414" height="760"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="g5C-Wg-DEu">
<rect key="frame" x="16" y="836" width="382" height="30"/>
<rect key="frame" x="16" y="788" width="382" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="CFr-4Z-X1A"/>
</constraints>
@@ -404,7 +397,7 @@
<tabBarItem key="tabBarItem" title="Settings" image="tabbar-icon-settings" id="5VO-yI-kw5"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="B9g-HM-VPb">
<rect key="frame" x="0.0" y="48" width="414" height="96"/>
<rect key="frame" x="0.0" y="96" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
@@ -506,7 +499,7 @@
<tabBarItem key="tabBarItem" title="Reports" image="tabbar-icon-reports" id="oaL-SG-Zpq"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="xbw-SF-Eq7">
<rect key="frame" x="0.0" y="48" width="414" height="96"/>
<rect key="frame" x="0.0" y="96" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
@@ -525,7 +518,7 @@
<tabBarItem key="tabBarItem" title="Alerts" image="tabbar-icon-alerts" id="aeo-GH-qCD"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="bk1-MS-moG">
<rect key="frame" x="0.0" y="48" width="414" height="96"/>
<rect key="frame" x="0.0" y="96" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
@@ -544,7 +537,7 @@
<tabBarItem key="tabBarItem" title="Seismic Networks" image="tabbar-icon-networks" id="eed-sY-0Ua"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="LRX-7t-Fk4">
<rect key="frame" x="0.0" y="48" width="414" height="96"/>
<rect key="frame" x="0.0" y="96" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
@@ -565,7 +558,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="200" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="a35-sg-TCr">
<rect key="frame" x="0.0" y="144" width="414" height="614"/>
<rect key="frame" x="0.0" y="192" width="414" height="532"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<outlet property="dataSource" destination="syj-UE-OWc" id="Vah-TU-YfT"/>
@@ -573,7 +566,7 @@
</connections>
</tableView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="PyX-yA-DXv">
<rect key="frame" x="0.0" y="758" width="414" height="55"/>
<rect key="frame" x="0.0" y="724" width="414" height="55"/>
<color key="backgroundColor" systemColor="groupTableViewBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="55" id="kmt-E8-s2w"/>
@@ -621,10 +614,9 @@
</scene>
</scenes>
<resources>
<image name="1.square" catalog="system" width="128" height="114"/>
<image name="navbar-icon-arrow-collapse" width="24" height="24"/>
<image name="navbar-icon-filters" width="24" height="24"/>
<image name="navbar-icon-refresh" width="24" height="24"/>
<image name="navbar-icon-sort" width="24" height="24"/>
<image name="tabbar-icon-alerts" width="25" height="25"/>
<image name="tabbar-icon-networks" width="25" height="25"/>
<image name="tabbar-icon-reports" width="25" height="25"/>
@@ -20,8 +20,12 @@ extension CGFloat {
static let cardVerticalSpacing: CGFloat = 10.0
}
@objc
class EQNBaseContainerTableViewCell: UITableViewCell {
/// Inset to apply to enclosing table view. Used to adjust spacing around the cells (for instance, top space is smaller that the space between the cards)
@objc static let EdgeInsets: UIEdgeInsets = .init(top: CGFloat.cardHorizontalMargin - CGFloat.cardVerticalMargin, left: 0, bottom: 0, right: 0)
// MARK: - UI
private lazy var internalContainerView: UIView = {
@@ -46,11 +50,11 @@ class EQNBaseContainerTableViewCell: UITableViewCell {
}()
private lazy var headerLabel: UILabel = {
let label = EQNEdgeInsetLabel()
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = AppTheme.Colors.lightBlue
label.font = .systemFont(ofSize: 17.0, weight: .medium)
label.textColor = .white
label.textAlignment = .center
label.font = .preferredFont(forTextStyle: .title2)
label.textColor = AppTheme.shared.cardTextColor
return label
}()
@@ -136,9 +140,9 @@ class EQNBaseContainerTableViewCell: UITableViewCell {
if isHeaderVisible {
containerView.addSubview(headerLabel)
headerLabel.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
headerLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: .cardPadding).isActive = true
headerLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
headerLabel.heightAnchor.constraint(equalToConstant: 26.0).isActive = true
headerLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
} else {
containerView.addSubview(emptyTopView)
emptyTopView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
@@ -7,6 +7,7 @@
//
import Foundation
import UIKit
class EQNBlurredCloseButton: UIButton {
@@ -18,6 +19,12 @@ class EQNBlurredCloseButton: UIButton {
setupUI()
}
override func awakeFromNib() {
super.awakeFromNib()
setupUI()
}
// MARK: - Private
private func setupUI() {
@@ -18,7 +18,7 @@ class EQNSeismicAnnotationView: MKAnnotationView {
private static let FullViewHeight: CGFloat = MagnitudeHeight + 2*LabelHeight
private static let FullViewWidth: CGFloat = 100.0
private static let SmallViewHeight: CGFloat = MagnitudeHeight
private static let SmallViewWidth: CGFloat = 100.0
private static let SmallViewWidth: CGFloat = 80.0
static let CircleViewHeight: CGFloat = 20.0
// MARK: - Public
@@ -80,7 +80,7 @@ class EQNSeismicAnnotationView: MKAnnotationView {
private lazy var magnitudeLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12, weight: .bold)
label.font = UIFont.systemFont(ofSize: 11, weight: .bold)
label.textAlignment = .center
label.textColor = AppTheme.Colors.lightBlue
return label
@@ -77,6 +77,7 @@
"status_cancel" = "ألغي";
"options_alarms" = "تنبيه بوقت فعلي";
"options_notification_enable_alarm" = "قم بتفعيل الإنذار عند اكتشاف زلزال في الوقت الفعلي بواسطة شبكة الهواتف الذكية";
"options_notification_disable_sound" = "قم بتعطيل صوت المنبه إذا كان الاهتزاز في موقعك خفيفًا";
"options_notification_official" = "إخطارات الشبكة الزلزالية";
"options_notification_enable_official" = "تلقي إشعارات بشأن الزلازل التي اكتشفتها شبكات الزلازل الوطنية والدولية";
"options_official_type" = "مرشح الإشعارات";
@@ -107,7 +108,7 @@
"permission_location_no_background_solve" = "حل المشكلة";
"main_share_app" = "شارك التطبيق";
"main_vote" = "قيّم التطبيق";
"main_twitter_see" = "التبيين في تويتر";
"main_twitter_see" = "عرض على X";
"map_smartphone_magnitude" = "سيتم تقدير قوة الزلزال بواسطة الشبكة الزلزالية الوطنية وسيظهر في علامة تبويب الشبكات الزلزالية في التطبيق";
"main_alerttest" = "تنبيه اختياري";
"main_simulator" = "محاكي";
@@ -235,3 +236,4 @@
"subscription_plan_monthly" = "شهريا";
"subscription_plan_yearly" = "سنوي";
"subscription_plan_perpetual" = "حياة";
"tap_to_open" = "انقر للتكبير";
@@ -77,6 +77,7 @@
"status_cancel" = "Διαγραφή";
"options_alarms" = "Ειδοποίηση σε πραγματικό χρόνο";
"options_notification_enable_alarm" = "Να ακούγεται συναγερμός όταν ανιχνεύεται σεισμός σε πραγματικό χρόνο από το δίκτυο smartphone";
"options_notification_disable_sound" = "Απενεργοποιήστε τον ήχο συναγερμού εάν το κούνημα στην τοποθεσία σας είναι ήπιο";
"options_notification_official" = "Κοινοποιήσεις σεισμικών δικτύων";
"options_notification_enable_official" = "Λήψη ειδοποιήσεων για τους σεισμούς που εντοπίζονται από τα εθνικά και διεθνή σεισμικά δίκτυα";
"options_official_type" = "Φίλτρο ειδοποιήσεων";
@@ -107,7 +108,7 @@
"permission_location_no_background_solve" = "Διόρθωση";
"main_share_app" = "Μοιράσου";
"main_vote" = "Ψήφισε την Εφαρμογή";
"main_twitter_see" = "Twitter";
"main_twitter_see" = "X";
"map_smartphone_magnitude" = "Το μέγεθος θα εκτιμηθεί από το εθνικό σεισμικό δίκτυο και θα εμφανιστεί στην οθόνη Σεισμικών Δικτύων της εφαρμογής";
"main_alerttest" = "Τεστ ειδοποίησης";
"main_simulator" = "Προσομοιωτής";
@@ -235,3 +236,4 @@
"subscription_plan_monthly" = "Μηνιαίο";
"subscription_plan_yearly" = "Ετήσιο";
"subscription_plan_perpetual" = "Διάρκεια Ζωής";
"tap_to_open" = "Πατήστε για μεγέθυνση";
@@ -77,6 +77,7 @@
"status_cancel" = "Cancel";
"options_alarms" = "Real time alert";
"options_notification_enable_alarm" = "Activate an alarm when a quake is detected in real time by the network of smartphones";
"options_notification_disable_sound" = "Disable the alarm sound if the shaking at your location is mild";
"options_notification_official" = "Seismic network notifications";
"options_notification_enable_official" = "Receive notifications for the earthquakes detected by the national and international seismic networks";
"options_official_type" = "Notification filter";
@@ -107,7 +108,7 @@
"permission_location_no_background_solve" = "Solve";
"main_share_app" = "Share App";
"main_vote" = "Rate the App";
"main_twitter_see" = "View on Twitter";
"main_twitter_see" = "View on X";
"map_smartphone_magnitude" = "The magnitude will be estimated by the national seismic network and it will appear in the Seismic Networks tab of the app";
"main_alerttest" = "Test alert";
"main_simulator" = "Simulator";
@@ -235,3 +236,4 @@
"subscription_plan_monthly" = "Monthly";
"subscription_plan_yearly" = "Annual";
"subscription_plan_perpetual" = "Lifetime";
"tap_to_open" = "Tap to open";
@@ -77,6 +77,7 @@
"status_cancel" = "Clara";
"options_alarms" = "Alerta en tiempo real";
"options_notification_enable_alarm" = "Suena una alarma cuando se detecta un sismo en tiempo real por la red de los teléfonos inteligentes";
"options_notification_disable_sound" = "Desactive el sonido de la alarma si el sismo en tu ubicación es leve";
"options_notification_official" = "Notificaciones redes sísmicas";
"options_notification_enable_official" = "Recibe las notificaciones de los sismos detectados por las redes sísmicas nacionales e internacionales";
"options_official_type" = "Filtro de notificaciones";
@@ -107,7 +108,7 @@
"permission_location_no_background_solve" = "Corregir";
"main_share_app" = "Comparte App";
"main_vote" = "Vota la app";
"main_twitter_see" = "Ver en Twitter";
"main_twitter_see" = "Ver en X";
"map_smartphone_magnitude" = "La magnitud será comunicada por la red sísmica nacional y aparecerá en la sección de Redes Sísmicas de la app";
"main_alerttest" = "Prueba alerta";
"main_simulator" = "Simulador";
@@ -235,3 +236,4 @@
"subscription_plan_monthly" = "Mensual";
"subscription_plan_yearly" = "Anual";
"subscription_plan_perpetual" = "Para siempre";
"tap_to_open" = "Toque para ampliar";
@@ -77,6 +77,7 @@
"status_cancel" = "Supprimer";
"options_alarms" = "Alerte en temps réel";
"options_notification_enable_alarm" = "Une alarme retentit lorsqu'un séisme est détecté par le réseau des smartphones";
"options_notification_disable_sound" = "Désactivez le son de l'alarme si les secousses à votre emplacement sont légères";
"options_notification_official" = "Notifications de réseaux sismiquex";
"options_notification_enable_official" = "Recevez les notifications des séismes détectés par les réseaux sismiques nationaux et internationaux";
"options_official_type" = "Filtre de notificatio";
@@ -107,7 +108,7 @@
"permission_location_no_background_solve" = "Corriger";
"main_share_app" = "Partager l'App";
"main_vote" = "Voter l'App";
"main_twitter_see" = "Voir sur Twitter";
"main_twitter_see" = "Voir sur X";
"map_smartphone_magnitude" = "La magnitude sera estimée par le réseau sismique national et s'affichera dans la page d'écran Réseaux Sismiques de l'app";
"main_alerttest" = "Test alerte";
"main_simulator" = "Simulateur";
@@ -235,3 +236,4 @@
"subscription_plan_monthly" = "Mensuel";
"subscription_plan_yearly" = "Annuel";
"subscription_plan_perpetual" = "Pour toujours";
"tap_to_open" = "Appuyez pour agrandir";
@@ -77,6 +77,7 @@
"status_cancel" = "Poništi";
"options_alarms" = "Upozorenje u stvarnom vremenu";
"options_notification_enable_alarm" = "Aktiviraj alarm kada mreža pametnih telefona u stvarnom vremenu otkrije potres";
"options_notification_disable_sound" = "Onemogućite zvuk alarma ako je podrhtavanje na vašoj lokaciji blago";
"options_notification_official" = "Obavijesti o seizmološkim mrežama";
"options_notification_enable_official" = "Primajte obavijesti o potresima koje su otkrile nacionalne i međunarodne seizmičke mreže";
"options_official_type" = "Filter obavijesti";
@@ -235,3 +236,4 @@
"subscription_plan_monthly" = "Mjesečno";
"subscription_plan_yearly" = "Godišnji";
"subscription_plan_perpetual" = "Zauvijek";
"tap_to_open" = "Dodirnite za uvećanje";
@@ -77,6 +77,7 @@
"status_cancel" = "Batal";
"options_alarms" = "Peringatan real time";
"options_notification_enable_alarm" = "Nyalakan alarm saat gempa terdeteksi secara real time oleh jaringan smartphone";
"options_notification_disable_sound" = "Nonaktifkan suara alarm jika guncangan di lokasi Anda ringan";
"options_notification_official" = "Pemberitahuan jaringan seismik";
"options_notification_enable_official" = "Terima pemberitahuan untuk gempa bumi yang terdeteksi oleh jaringan seismik nasional dan internasional";
"options_official_type" = "Filter notifikasi";
@@ -107,7 +108,7 @@
"permission_location_no_background_solve" = "Atasi";
"main_share_app" = "Bagikan Aplikasi";
"main_vote" = "Beri Rating untuk Aplikasi";
"main_twitter_see" = "Lihat di Twitter";
"main_twitter_see" = "Lihat di X";
"map_smartphone_magnitude" = "Magnitudo akan diperkirakan oleh jaringan seismik nasional dan akan muncul di tab Jaringan Seismik pada aplikasi";
"main_alerttest" = "Tes peringatan";
"main_simulator" = "Simulator";
@@ -235,3 +236,4 @@
"subscription_plan_monthly" = "Bulanan";
"subscription_plan_yearly" = "Tahunan";
"subscription_plan_perpetual" = "Selamanya";
"tap_to_open" = "Ketuk untuk memperbesar";
@@ -77,6 +77,7 @@
"status_cancel" = "Cancella";
"options_alarms" = "Allerta in tempo reale";
"options_notification_enable_alarm" = "Attiva un allarme quando un sisma è rilevato dalla rete smartphone";
"options_notification_disable_sound" = "Disabilita il suono di allerta se l'intensità del sisma nella tua zona è debole";
"options_notification_official" = "Notifiche da reti sismiche";
"options_notification_enable_official" = "Ricevi notifiche per i sismi rilevati dalle reti sismiche nazionali e internazionali";
"options_official_type" = "Filtro notifiche";
@@ -107,7 +108,7 @@
"permission_location_no_background_solve" = "Correggi";
"main_share_app" = "Condividi App";
"main_vote" = "Vota l'App";
"main_twitter_see" = "Vedi in Twitter";
"main_twitter_see" = "Vedi in X";
"map_smartphone_magnitude" = "La magnitudo sarà comunicata dalla rete sismica nazionale e comparirà nella sezione Reti Sismiche dell'app";
"main_alerttest" = "Test Allerta";
"main_simulator" = "Simulatore";
@@ -235,3 +236,4 @@
"subscription_plan_monthly" = "Mensile";
"subscription_plan_yearly" = "Annuale";
"subscription_plan_perpetual" = "A vita";
"tap_to_open" = "Tocca per aprire";
@@ -77,6 +77,7 @@
"status_cancel" = "İptal et";
"options_alarms" = "Gerçek zamanlı uyarı";
"options_notification_enable_alarm" = "Bir deprem akıllı telefon ağı tarafından gerçek zamanlı olarak tespit edildiğinde bir alarm çal";
"options_notification_disable_sound" = "Bulunduğunuz yerdeki sarsıntı hafifse alarm sesini devre dışı bırakın";
"options_notification_official" = "Sismik ağ bildirimleri";
"options_notification_enable_official" = "Ulusal ve uluslararası sismik ağlar tarafından tespit edilen depremler için bildirim alın";
"options_official_type" = "Bildirim filtresi";
@@ -107,7 +108,7 @@
"permission_location_no_background_solve" = "Çözün";
"main_share_app" = "Uygul. Paylaş";
"main_vote" = "Uygulamayı oyla";
"main_twitter_see" = "Twitter";
"main_twitter_see" = "X";
"map_smartphone_magnitude" = "Büyüklük ulusal sismik ağ tarafından tahmin edilecek ve uygulamanın Sismik Ağlar sekmesinde görünecektir.";
"main_alerttest" = "Test uyarısı";
"main_simulator" = "Simülatör";
@@ -235,3 +236,4 @@
"subscription_plan_monthly" = "Aylık";
"subscription_plan_yearly" = "Yıllık";
"subscription_plan_perpetual" = "Sonsuza kadar";
"tap_to_open" = "Büyütmek için dokunun";