Compare commits

...

170 Commits

Author SHA1 Message Date
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
Andrea Busi b12f83680a release: Increase version for release 2025-03-06 09:02:38 +01:00
Andrea Busi ee827c41ae feat: Show callout for shakemap annotations 2025-02-28 16:49:51 +01:00
Andrea Busi d0d06394f0 feat: Save selected map pin style 2025-02-28 16:49:36 +01:00
Andrea Busi b933b900ed release: Increase version for release 2025-02-28 12:54:40 +01:00
Andrea Busi 0e7de44332 refactor: Move source position in seismic card 2025-02-28 12:54:40 +01:00
Andrea Busi 547bb794f0 feat: Add intensity map 2025-02-28 12:54:40 +01:00
Andrea Busi 9b1f1f12d2 feat: Add new APIService to handle network requests 2025-02-27 16:15:37 +01:00
Andrea Busi 7fc324367d feat: Add Log class 2025-02-27 16:15:37 +01:00
Andrea Busi 3cb712f709 fix: Set 2 as min reported user for felt filter 2025-02-25 16:40:41 +01:00
Andrea Busi 993e2924c7 release: Increase version for release 2025-02-21 16:28:48 +01:00
Andrea Busi a167c989cc feat: Add filter for "user felt"
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/81
2025-02-21 15:13:00 +01:00
Andrea Busi 1b50f4fd17 fix: Set locale in date formatters, to solve parsing issues with 0-12 hours 2025-02-20 15:16:59 +01:00
Andrea Busi 0003b4607c refactor: Make filters bigger 2025-02-20 15:16:42 +01:00
Andrea Busi 85c9f333ce fix: Hide scroll bar in seismics table 2025-02-12 09:06:25 +01:00
Andrea Busi 217cbfd4e3 release: Increase version for release 2025-02-11 15:51:15 +01:00
Andrea Busi 5d8de1fb36 feat: Improve scroll indicator with tons of rectangles 2025-02-11 15:50:20 +01:00
Andrea Busi f23bb78ceb fix: Reset center position when data changes 2025-02-11 15:50:20 +01:00
Andrea Busi 0d91954614 feat: Upgrade to Xcode recommended settings 2025-02-11 14:58:35 +01:00
Andrea Busi 49f5fa91fe release: Increase version for release 2025-02-06 14:10:27 +01:00
Andrea Busi 68e560768b refactor: Replace renamed APIs for GoogleAds SDK 2025-02-06 12:00:26 +01:00
Andrea Busi 3e9c319b50 dependency: Bump GoogleAds SDK to v12 2025-02-06 12:00:26 +01:00
Andrea Busi d35e0e1b4a refactor: Replace deprecated API for Facebook SDK 2025-02-06 12:00:26 +01:00
Andrea Busi 6ede137ef7 dependency: Bump Facebook SDK to v18 2025-02-06 12:00:19 +01:00
Andrea Busi c94195d48e dependency: Update repo url for Shogun 2025-02-06 11:53:53 +01:00
Andrea Busi 28919d7b72 release: Increase version for release 2025-02-06 11:53:44 +01:00
Andrea Busi a239534b91 feat: Add scroll indicator view in seismic list 2025-02-06 11:33:22 +01:00
Andrea Busi 226342f36c dependency: Bump Firebase 2025-02-06 11:33:22 +01:00
Andrea Busi ca6afbec5f refactor: Delete workspace file, no longer used 2025-01-31 14:38:26 +01:00
Andrea Busi 465d3e8013 release: Increase version for release 2024-10-17 18:48:19 +02:00
Andrea Busi a7e88b43f5 fix: Add LSMinimumSystemVersion for macOS compatibility 2024-10-17 18:47:37 +02:00
Andrea Busi 57ef877846 release: Increase version for release 2024-10-17 17:35:57 +02:00
Andrea Busi c44d97b9fb feat: Disable critical alerts setting if permission is not granted 2024-10-17 17:22:41 +02:00
Andrea Busi fd4ed7f66f fix: Load existing Firebase Token 2024-10-17 16:05:17 +02:00
Andrea Busi ef5db97854 refactor: Store first app start using a user default 2024-10-17 16:04:56 +02:00
Andrea Busi ce0e17a0c5 refactor: Remove old migrations 2024-10-17 15:21:18 +02:00
Andrea Busi 2a46f1d2d6 release: Increase version for release 2024-10-17 09:21:44 +02:00
Andrea Busi 93871f0358 chore: Add IDE file 2024-10-17 09:19:10 +02:00
Andrea Busi 3e8fe0680d fix: Solve missing critical alert permission request 2024-10-17 09:18:52 +02:00
Andrea Busi 6be5f72360 release: Increase version for release 2024-07-16 11:45:52 +02:00
Andrea Busi ccd1b9de59 dependency: Update Firebase 2024-07-16 11:45:43 +02:00
Andrea Busi 5737eb5b02 feat: Sort user subscriptions (top10k first) 2024-07-16 11:43:58 +02:00
Andrea Busi c549bb6ea5 fix: Solve wrong localized AR string 2024-07-16 09:23:39 +02:00
Andrea Busi ff80905033 fix: Solve crash due to wrong string format 2024-07-16 09:09:57 +02:00
Andrea Busi dad2bc5648 release: Increase version for release 2024-07-08 15:04:01 +02:00
Andrea Busi 10c74e278e refactor: Rework layout for restore subscriptions 2024-07-08 13:51:54 +02:00
Andrea Busi 96dbf960d2 refactor: Change tab official translations 2024-07-08 13:51:39 +02:00
Andrea Busi 81bfdd02a6 release: Increase version for release 2024-07-05 11:51:52 +02:00
Andrea Busi 2ab3267981 dependency: SPM 2024-07-05 11:45:29 +02:00
Andrea Busi 48b6941ed5 feat: Change nav bar color 2024-07-05 11:45:23 +02:00
Andrea Busi 669cb3c4f3 fix: Improve translation 2024-07-05 11:40:52 +02:00
Andrea Busi 638d819d35 refactor: Improve log 2024-07-05 09:03:48 +02:00
Andrea Busi a9884d8a8d release: Increase version for release 2024-07-04 15:22:58 +02:00
Andrea Busi 2ef3560011 feat: Scroll to opened seismic 2024-07-04 15:20:05 +02:00
Andrea Busi 05093bb7a4 chore: Update push payloads 2024-07-04 15:19:55 +02:00
Andrea Busi 55f84ab46d feat: Add new string for notification body 2024-07-04 15:19:45 +02:00
Andrea Busi 03b4d0ddd6 feat: Show right arrow to priority cell 2024-07-03 11:18:50 +02:00
Andrea Busi 3c5f26bc94 fix: Set background color to the proper container 2024-07-03 11:18:41 +02:00
Andrea Busi 8c79d45b19 release: Increase version for release 2024-07-03 10:10:37 +02:00
Andrea Busi 931d04c5e1 refactor: Reorganize files 2024-07-03 10:10:04 +02:00
Andrea Busi 4d62fbbbd3 fix: Solve wrong distance in filter evaluation 2024-07-03 10:00:55 +02:00
Andrea Busi 1c7065ece7 release: Increase version for release 2024-07-02 20:59:37 +02:00
Andrea Busi 6dfa51e013 refactor: Bigger fonts 2024-07-02 19:13:10 +02:00
Andrea Busi b8b21d1458 fix: Remove separator from table view 2024-07-02 18:15:03 +02:00
Andrea Busi 88317f79e8 fix: Missing rounded corners 2024-07-02 17:57:01 +02:00
Andrea Busi 4e1147e782 refactor: Remove no longer used class 2024-07-02 17:56:55 +02:00
Andrea Busi 579969d507 fix: Missing callback 2024-07-02 17:55:25 +02:00
Andrea Busi 4d991d9a10 refactor: Recreate expanded notification cell via code and change some UI elements 2024-07-02 17:55:19 +02:00
Andrea Busi 41491b5ee7 refactor: Change colors as per specifications 2024-07-02 12:25:18 +02:00
Andrea Busi 197b375c28 refactor: Remove setting for filter type in seismic notifications 2024-07-01 11:18:47 +02:00
Andrea Busi f41e6b50ec refactor: Create UI in code to properly manage filter view 2024-07-01 10:40:50 +02:00
Andrea Busi 796e4b5895 release: Increase version for release 2024-07-01 10:40:26 +02:00
Andrea Busi e43a93979d fix: Solve layout issue with gradient background 2024-06-29 16:16:47 +02:00
Andrea Busi ef1aaa7d71 refactor: Use extension for view rounded corners and shadow 2024-06-29 16:16:47 +02:00
Andrea Busi 22d78baa8a feat: Highlight seismic card title for push notification
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/76
2024-06-29 16:16:47 +02:00
Andrea Busi e4588aa731 chore: Update payload for official push notification 2024-06-29 16:16:47 +02:00
Andrea Busi 07764f91ed feat: Add logic to update filter when push is opened
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/76
2024-06-29 16:16:47 +02:00
Andrea Busi a0a238e384 refactor" Don't show image for official notifications 2024-06-27 18:19:30 +02:00
Andrea Busi e61a45f78f fix: Resolve deprecation 2024-06-27 17:04:02 +02:00
Andrea Busi 0fdc60b938 chore: Fix push payload sample 2024-06-27 17:04:02 +02:00
Andrea Busi 5f02e2b8bb dependency: Update Firebase 2024-06-27 17:04:02 +02:00
Andrea Busi b17a57b98e refactor: Remove unused strings 2024-06-24 17:47:54 +02:00
Andrea Busi 2379077272 release: Increase version for release 2024-06-24 09:02:50 +02:00
Andrea Busi 78f0cfb2fa feat: Add gradient background in seismic cards
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/74
2024-06-24 09:00:49 +02:00
Andrea Busi f6bfe3fca0 dependency: Update Shogun 2024-06-24 09:00:16 +02:00
Andrea Busi d5ab49b807 release: Increase version for release 2024-06-23 16:33:19 +02:00
Andrea Busi b8bd547d65 refactor: Minor changes to new map 2024-06-23 16:24:31 +02:00
Andrea Busi 547c503726 release: Increase version for release 2024-06-22 11:06:07 +02:00
Andrea Busi 234622bcfd feat: Share screenshot feature in base map controller 2024-06-22 11:04:27 +02:00
Andrea Busi 589466c8c6 refactor: Minor changes in subscription page 2024-06-22 11:04:27 +02:00
Andrea Busi 2e7742951e feat: Change map annotation layout in seismic map
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/71
2024-06-22 11:04:27 +02:00
Andrea Busi 3ed77ff1af feat: Support zPriority in base map 2024-06-21 17:42:15 +02:00
Andrea Busi c98530fc54 release: Increase version for release 2024-06-21 15:59:57 +02:00
Andrea Busi 98cc7e7c4c feat: Complete subscription page refactor 2024-06-21 15:56:55 +02:00
Andrea Busi 4db0bb6316 feat: Base cell can show a right chevron 2024-06-20 16:26:19 +02:00
Andrea Busi 8c3f2dad6d refactor: Rework model for in app products 2024-06-20 16:26:19 +02:00
Andrea Busi e0f346a4dc refactor: Create subscriptions controller from code 2024-06-20 16:26:19 +02:00
Andrea Busi eac0f8249e feat: New layout for subscription details 2024-06-20 16:26:19 +02:00
Andrea Busi d7c691101c refactor: Use methods from Shogun 2024-06-18 23:43:34 +02:00
Andrea Busi 49edbe1a14 refactor: Don't use static constants for fonts 2024-06-18 15:33:58 +02:00
Andrea Busi c5b3750ee7 release: Increase version for release 2024-06-15 15:21:31 +02:00
Andrea Busi 98fb65a640 refactor: Remove some unused strings 2024-06-15 15:20:18 +02:00
Andrea Busi c20041127b feat: Don't open filters from map and change displayed label
https://gitlab.steamware.net/eqn/eqn.ios/-/issues/71
2024-06-15 15:16:24 +02:00
Andrea Busi 3995c29b22 refactor: Migrate Segnalazioni cells to code 2024-06-15 15:15:42 +02:00
Andrea Busi dfa07d0d10 fix: Solve wrong seismic download filter 2024-06-14 21:57:39 +02:00
Andrea Busi ce6fbb24ff release: Increase version for release 2024-06-14 16:30:07 +02:00
Andrea Busi 382dcfa794 refactor: Migrate subscription products cell to code 2024-06-14 16:15:26 +02:00
Andrea Busi d46a2e1559 refactor: Create extensions for padding constants 2024-06-14 16:12:21 +02:00
Andrea Busi b0d1cde42b refactor: Migarate active subscription and description to code 2024-06-14 16:03:00 +02:00
Andrea Busi d8612e33a3 refactor: Migrate AlertsPositionDataTableViewCell to code 2024-06-14 16:03:00 +02:00
Andrea Busi 73826d7520 refactor: Migrate AlertsSeismicNotificationCompactTableViewCell to code 2024-06-14 16:03:00 +02:00
Andrea Busi 3f57ac9b96 refactor: Migrate AlertsPastEartquakesTableViewCell to code 2024-06-14 16:03:00 +02:00
Andrea Busi 54c78aac0f refactor: Migrate AlertsNoLocationTableViewCell to code 2024-06-14 16:03:00 +02:00
Andrea Busi 975f5ed5bc refactor: Migrate AlertsPriorityServiceTableViewCell to code 2024-06-14 16:03:00 +02:00
Andrea Busi 52142486cf refactor: Migrate AlertsSmartphoneNetworkTableViewCell to code 2024-06-14 16:03:00 +02:00
Andrea Busi b4b676ca8d refactor: Add extension to create EQNRoundedButton 2024-06-14 13:09:48 +02:00
Andrea Busi dd9ef878e2 feat: Support multiline titles in EQNRoundedButton 2024-06-14 12:11:48 +02:00
Andrea Busi 5b978e535c release: Increase version for release 2024-06-12 22:23:09 +02:00
Andrea Busi 242c15ba58 feat: Support dynamic font in some views 2024-06-12 22:22:35 +02:00
Andrea Busi a224837dcb fix: Some UI fixes 2024-06-11 17:40:33 +02:00
Andrea Busi a21c16a01c release: Increase version for release 2024-06-11 11:17:47 +02:00
Andrea Busi 1496f25251 feat: Manage missing location in Seismic list and filters 2024-06-11 11:13:40 +02:00
Andrea Busi ad6eb6619c refactor: Remove no longer needed ObjC bridging 2024-06-11 10:56:31 +02:00
Andrea Busi f9a8dffad5 feat: Minor UI change for depth
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/69
2024-06-10 22:20:14 +02:00
Andrea Busi a708a0f79a refactor: Migrate EQNSettingRealTimeAlert to Swift 2024-06-10 22:14:01 +02:00
Andrea Busi 49431a760c feat: Align user report to new values and recreate in Swift 2024-06-10 22:14:01 +02:00
Andrea Busi a57e883409 refactor: Align names for "seismic network notifications" settings 2024-06-10 15:01:23 +02:00
Andrea Busi b373dc1d60 refactor: Migrate SettingsRealTimeAlertsViewController to Swift 2024-06-10 15:01:23 +02:00
Andrea Busi 01f1df9c01 feat: Force notification settings upload on first app start after migration
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/68
2024-06-10 13:25:34 +02:00
Andrea Busi 72441d0532 refactor: Create Swift version of SettingsBaseViewController 2024-06-10 13:25:34 +02:00
Andrea Busi a4afb84e6d feat: Add sort feature in Seismic list 2024-06-10 08:55:25 +02:00
Andrea Busi 45a59e30ba feat: Rework seismic filters with new specifications
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/70
2024-06-09 16:15:34 +02:00
Andrea Busi dac13acb9e refactor: Update parameters for upload settings
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/68
2024-06-09 16:15:34 +02:00
Andrea Busi a9e264d666 feat: Rework notification settings for network alerts
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/66
2024-06-09 16:15:34 +02:00
Andrea Busi e64aaf2469 refactor: Move network label in card
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/69
2024-06-09 16:15:34 +02:00
Andrea Busi 30c7536d4c refactor: Remove bell icon in Seismic Networks
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/69
2024-06-09 16:15:34 +02:00
Andrea Busi 70e82a67b1 refactor: Remove networks selection in Seismic Networks
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/69
2024-06-09 16:15:34 +02:00
Andrea Busi f020ac70a1 feat: Increase minimum target to iOS 14 2024-06-09 16:15:12 +02:00
Andrea Busi dc4ccd796d dependency: Update packages 2024-06-09 16:15:05 +02:00
Andrea Busi f66d6558b5 refactor: Move getDeltaMinute to EQNUtility 2024-06-07 17:03:13 +02:00
Andrea Busi 536ed32fb9 refactor: Remove unused constants 2024-06-07 17:03:08 +02:00
Andrea Busi 2e1a2a8e04 feat: Add method to retrieve enum from user defaults 2024-06-06 14:50:32 +02:00
Andrea Busi 527132b7eb dependency: Update Shogun 2024-06-06 14:46:53 +02:00
Andrea Busi 8cf69a9d12 fix: Don't perform user registration if Firebase token is null
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/65
2024-06-06 10:54:31 +02:00
Andrea Busi 6cba42994d refactor: Remove CocoaPods 2024-06-06 10:41:57 +02:00
Andrea Busi fd7821c083 dependency: Migrate Firebase to SPM 2024-06-06 10:41:57 +02:00
Andrea Busi 5fab419d0e refactor: Use new property from Facebook SDK 2024-04-29 14:25:34 +02:00
Andrea Busi befe46465b dependency: Migrate FBSDKCoreKit to SPM 2024-04-29 14:25:34 +02:00
Andrea Busi 5e6ee892ce dependency: Migrate Google-Mobile-Ads-SDK to SPM 2024-04-29 14:25:34 +02:00
Andrea Busi 79d4b3b3bd refactor: Change nullability 2024-04-29 14:25:34 +02:00
Andrea Busi 357bdd47e3 dependency: Migrate DZNEmptyDataSet to SPM 2024-04-29 14:25:34 +02:00
Andrea Busi 1e4dd507da dependency: Migrate Solar as SPM 2024-04-29 14:25:34 +02:00
142 changed files with 6509 additions and 5777 deletions
+16
View File
@@ -1,5 +1,21 @@
# Changelog
## Versione 5.9
- Aggiunta barra laterale in lista sismi
- Aggiunto filtro "sismi percepiti"
- Aggiunta "Mappa intensità"
## Versione 5.8.1
- Corrette traduzioni errate (causavano crash)
- Aggiunto ordinamento in sottoscrizioni utente (prima Top10k)
## Versione 5.8
- Modifica algoritmo filtro lista (issue #70)
- Modifica impostazioni "Notifiche da reti sismiche" (issue #66)
- Modifica invio impostazioni app per notifiche (issue #68)
- Modifica impostazioni "Notifiche segnalazioni utente" (issue #67)
- Modifica tab Reti Sismiche (issue #69)
## Versione 5.7
- Aumentato target ad iOS 13
- Disattivata logica calibrazione/monitoraggio
+34
View File
@@ -0,0 +1,34 @@
{
"magnitude_range" : "0",
"google.c.a.e" : "1",
"provider" : "INGV",
"google.c.fid" : "fFjFx_Em8E-op_zHYXZpSr",
"preliminary" : "0",
"longitude" : "15.2917",
"gcm.message_id" : "1719991146578422",
"latitude" : "40.7738",
"google.c.sender.id" : "899482329945",
"type" : "official",
"magnitude" : "1.4",
"difference" : "13",
"depth" : "14.6",
"aps" : {
"mutable-content" : 1,
"alert" : {
"body" : "Sisma rilevato a",
"title-loc-key" : "Segnalazione da rete sismica",
"title" : "Segnalazione da rete sismica",
"loc-key" : "Sisma rilevato a",
"action-loc-key" : "",
"loc-args" : [
"2 km SW Laviano (SA) - M1.4"
]
},
"content-available" : 1,
"sound" : "default"
},
"data" : "2024-07-03 09:05:52",
"magnitude_type" : "ML",
"place" : "2 km SW Laviano (SA)",
"pop100" : "6824"
}
-31
View File
@@ -1,31 +0,0 @@
{
"magnitude_range" : "0",
"provider" : "SGC",
"google.c.a.e" : "1",
"google.c.fid" : "d3PS1dEvrUA-tmLLpl5E5f",
"preliminary" : "0",
"longitude" : "-75.5157",
"gcm.message_id" : "1668682445010677",
"latitude" : "4.35306",
"type" : "official",
"google.c.sender.id" : "899482329945",
"difference" : "6",
"data" : "2022-11-17 11:48:00",
"depth" : "26",
"aps" : {
"content-available" : 1,
"alert" : {
"loc-key" : "Sisma rilevato a",
"title-loc-key" : "Segnalazione da rete sismica",
"loc-args" : [
"Cajamarca - Tolima, Colombia - M2.2"
]
},
"mutable-content" : 1,
"sound" : "default"
},
"magnitude" : "2.2",
"magnitude_type" : "M",
"place" : "Cajamarca - Tolima, Colombia",
"pop100" : "6622"
}
@@ -5,7 +5,7 @@
"loc-args": [
"2 km da Foligno"
],
"loc-key": "Rilevato sisma forte a",
"loc-key": "Sisma segnalato da utente a",
"title-loc-key": "Allerta sismica in tempo reale"
},
"category": "notifica_con_mappa",
@@ -97,23 +97,8 @@ class NotificationService: UNNotificationServiceExtension {
// use a generic warning icon instead
iconName = "warning_yellow.png"
case "official":
let provider = userInfo.string(forKey: "provider", orDefault: "")
let intensity = userInfo.double(forKey: "magnitude", orDefault: 0)
let color: String
if intensity < 2.0 {
color = "_white"
} else if intensity < 3.5 {
color = "_green"
} else if intensity < 4.5 {
color = "_yellow"
} else if intensity < 5.5 {
color = "_red"
} else {
color = "_purple"
}
iconName = manualIconName(for: provider, color: color)
// don't show any images
break
default:
break
}
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,6 @@
<Workspace
version = "1.0">
<FileRef
location = "self:Earthquake Network.xcodeproj">
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,177 @@
{
"originHash" : "898a30d298491e1ce821191ebfa2e7d28e7c9ea4c119cbfdfb1b245bc94ac6c3",
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27",
"version" : "1.2024011602.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
"version" : "11.2.0"
}
},
{
"identity" : "dznemptydataset",
"kind" : "remoteSourceControl",
"location" : "https://github.com/dzenbot/DZNEmptyDataSet",
"state" : {
"branch" : "master",
"revision" : "9bffa69a83a9fa58a14b3cf43cb6dd8a63774179"
}
},
{
"identity" : "facebook-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/facebook/facebook-ios-sdk",
"state" : {
"revision" : "b28dde427715b45a26ebebf697929f4a81b15e04",
"version" : "18.0.0"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk.git",
"state" : {
"revision" : "075679d6b25b28f4cb167f1d7769b58fb556fb30",
"version" : "11.8.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "be0881ff728eca210ccb628092af400c086abda3",
"version" : "11.7.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
"version" : "8.0.2"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "f56d8fc3162de9a498377c7b6cea43431f4f5083",
"version" : "1.65.1"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b",
"version" : "3.5.0"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "2d12673670417654f08f5f90fdd62926dc3a2648",
"version" : "100.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.5"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{
"identity" : "shogun",
"kind" : "remoteSourceControl",
"location" : "https://github.com/andreabusi-it/Shogun",
"state" : {
"revision" : "ad890190d6be90f7712c2e56a38ef0937d9f8c1a",
"version" : "1.8.0"
}
},
{
"identity" : "solar",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ceeK/Solar.git",
"state" : {
"revision" : "c2b96f2d5fb7f835b91cefac5e83101f54643901",
"version" : "3.0.1"
}
},
{
"identity" : "swift-package-manager-google-mobile-ads",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/swift-package-manager-google-mobile-ads.git",
"state" : {
"revision" : "aa24c7dc03bca62c42747314dc8537f15587b50d",
"version" : "12.0.0"
}
},
{
"identity" : "swift-package-manager-google-user-messaging-platform",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/swift-package-manager-google-user-messaging-platform.git",
"state" : {
"revision" : "9b68aa69fb508f0274853e226c734151a973c7b7",
"version" : "2.4.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "9f0c76544701845ad98716f3f6a774a892152bcb",
"version" : "1.26.0"
}
}
],
"version" : 3
}
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1410"
LastUpgradeVersion = "1620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1410"
LastUpgradeVersion = "1620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
@@ -15,7 +15,7 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8C4B0B7921CACE3F00AED489"
BlueprintIdentifier = "65FFDC91292F672B00EA821B"
BuildableName = "EQNNotificationService.appex"
BlueprintName = "EQNNotificationService"
ReferencedContainer = "container:Earthquake Network.xcodeproj">
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1410"
LastUpgradeVersion = "1620"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Earthquake Network.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
@@ -1,14 +0,0 @@
{
"pins" : [
{
"identity" : "shogun",
"kind" : "remoteSourceControl",
"location" : "https://github.com/andreabusi-it/Shogun",
"state" : {
"revision" : "c164595fdd5d0771a6a24cbff85a7582f0f07311",
"version" : "1.3.0"
}
}
],
"version" : 2
}
+15 -17
View File
@@ -11,9 +11,6 @@
#import "EQNUser.h"
#import "EQNAccelerometroManager.h"
#import "EQNManager.h"
#import "EQNAllertaSismica.h"
#import "EQNNotificheSegnalazioniUtente.h"
#import "EQNNotificheReteSismiche.h"
#import "EQNMainTabBarController.h"
#import "NSDictionary+EQNExtensions.h"
@@ -152,7 +149,7 @@
[self handlePushNotificationWithNotificationContent:content];
// Change this to your preferred presentation option
completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound);
completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionSound);
}
// Handle notification messages after display notification is tapped by the user.
@@ -173,20 +170,22 @@
{
NSString *type = content.userInfo[@"type"];
// Store both original payload and modified title/body
// This will be usefull to avoid to re-evaluate logic for title display.
NSDictionary *notification = @{
@"title": content.title,
@"body": content.body,
@"userInfo": content.userInfo
};
EQNTabBarSection section = EQNTabBarSectionAllerte;
if ([type isEqualToString:@"eqn"]) {
// Store both original payload and modified title/body
// This will be usefull to avoid to re-evaluate logic for title display.
NSDictionary *notification = @{
@"title": content.title,
@"body": content.body,
@"userInfo": content.userInfo
};
if ([type isEqualToString:@"eqn"]) {
[EQNRealtimePushNotification storeNotificationWithPayload:notification];
section = EQNTabBarSectionAllerte;
} else if([type isEqualToString:@"manual"]) {
section = EQNTabBarSectionSegnalazioni;
} else if([type isEqualToString:@"official"]) {
[EQNOfficialPushNotification storeNotificationWithPayload:notification];
section = EQNTabBarSectionRetiSismiche;
}
@@ -233,8 +232,7 @@
- (void)configureFacebookSDKWithApplication:(UIApplication *)application andOptions:(NSDictionary *)launchOptions
{
[FBSDKApplicationDelegate.sharedInstance application:application didFinishLaunchingWithOptions:launchOptions];
[FBSDKSettings.sharedSettings setIsAdvertiserIDCollectionEnabled:YES];
[FBSDKSettings.sharedSettings setIsAdvertiserIDCollectionEnabled:YES];
FBSDKSettings.sharedSettings.isAdvertiserIDCollectionEnabled = YES;
}
#pragma mark - FIRMessagingDelegate
@@ -244,9 +242,9 @@
NSLog(@"[Firebase] fcmToken %@", fcmToken);
if (EQNUserData.sharedData.isFirstStart) {
// save default values for notification settings
[EQNAllertaSismica saveDefaultValues];
[EQNNotificheSegnalazioniUtente saveDefaultValues];
[EQNNotificheReteSismiche saveDefaultValues];
[EQNSettingRealTimeAlert saveDefaultValues];
[EQNSettingUserReportNotification saveDefaultValues];
[EQNSettingSeismicNetworkNotification saveDefaultValues];
}
[EQNUser.defaultUser registerUserIfNeededWithFirebaseToken:fcmToken];
+32 -17
View File
@@ -21,20 +21,12 @@ extension UserDefaults {
static let AllertaSismicaSismiDaNotificare = "NOTIFICHE_ALLERA_SISMICA_SISMI_DA_NOTIFICARE"
static let AllertaSismicaRaggioSismiLievi = "NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_LIEVI"
static let AllertaSismicaRaggioSismiForti = "NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_FORTI"
static let AllertaSismicaImpostaVolume = "NOTIFICHE_ALLERA_SISMICA_IMPOSTA_VOLUME"
static let AllertaSismicaTestaAllarma = "NOTIFICHE_ALLERA_SISMICA_TESTA_ALLARME"
static let AllertaSismicaAbilitaIntervallo = "NOTIFICHE_ALLERA_SISMICA_ABILITA_INTERVALLO"
static let AllertaSismicaOraInizio = "NOTIFICHE_ALLERA_SISMICA_ORA_INIZIO"
static let AllertaSismicaOraFine = "NOTIFICHE_ALLERA_SISMICA_ORA_INIZIO"
// Impostazioni della sezione `Notifiche da reti sismiche`
static let NotificheRetiSismicheAbilitato = "NOTIFICHE_ATTIVA_RETI_SISMICHE"
static let NotificheRetiSismicheViciniAbilitato = "NOTIFICHE_ATTIVA_RETI_SISMICHE_VICINE"
static let NotificheRetiSismicheTerremotiFortiAbilitato = "NOTIFICHE_ATTIVA_RETI_TERREMOTI_FORTI"
static let NotificheRetiSismicheDistanzaPosizione = "NOTIFICHE_DISTANZA_POSIZIONE_RETI_SISMICHE"
static let NotificheRetiSismicheEnergiaSisma = "NOTIFICHE_ATTIVA_RETI_ENERGIA_SISMI"
static let NotificheRetiSismicheEnergiaTerremotiForti = "NOTIFICHE_ATTIVA_RETI_ENERGIA_FORTI"
static let NotificheRetiSismicheListaEnti = "NOTIFICHE_ATTIVA_RETI_LISTA_ENTI"
static let NotificheRetiSismicheMagnitudoMinima = "NOTIFICHE_ATTIVA_RETI_ENERGIA_SISMI"
static let NotificheRetiSismicheDistanzaMassima = "NOTIFICHE_DISTANZA_POSIZIONE_RETI_SISMICHE"
static let NotificheRetiSismicheFiltroNotifiche = "NOTIFICHE_FILTRO_NOTIFICHE_RETI_SISMICHE"
// Impostazioni della sezione `Notifiche segnalazioni utente`
static let NotificheSegnalazioniUtenteAbilitato = "NOTIFICHE_SU_ATTIVA_SEGNALAZIONE_UTENTE"
@@ -45,6 +37,7 @@ extension UserDefaults {
static let UserReportCodeStatus = "CODE_MESSAGE_EQN"
// Proprietà e preferenze dell'utente
static let FirstAppStartExecuted = "EQNUserDefaultFirstAppStartExecuted"
/// Ultima posizione conosciuta dell'utente
static let UserDataLastLocation = "EQNLast_Location"
/// Token Firebase dell'utente corrente
@@ -63,23 +56,36 @@ extension UserDefaults {
static let UserReportExpandedView = "EQNData.UserReportExpandedView"
/// Se `true` visualizza le opzioni nella singole card in reti sismiche
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"
static let AppMigrationV5_4 = "EQNUserDefaultMigrationV5_4"
static let AppMigrationV5_8 = "EQNUserDefaultMigrationV5_8"
static let AppMigrationV5_8_2 = "EQNUserDefaultMigrationV5_8_2"
static let AppMigrationV5_9 = "EQNUserDefaultMigrationV5_9"
static let SettingsSeismicNetworkNotificationMigrationV5_8 = "EQNUserDefaultSettingsSeismicNetworkNotificationMigrationV5_8"
static let SettingsUserReportNotificationMigrationV5_8 = "EQNUserDefaultSettingsUserReportNotificationMigrationV5_8"
static let SismicFiltersMigrationV5_8 = "EQNUserDefaultSismicFiltersMigrationV5_8"
static let SaveSettingsNotificationMigrationV5_8 = "EQNUserDefaultSaveSettingsNotificationMigrationV5_8"
// Notifica allerta salvata
static let RealTimeAlertPayload = "EQNData.RealtimePushNotificationPayload"
static let RealTimeAlertDate = "EQNData.RealtimeAlertDate"
// Notifica rete sismica aperta
static let OfficialAlertPayload = "EQNData.OfficialPushNotificationPayload"
// Filtri sezioni reti sismiche
static let SeismicFilterOption = "EQN_SISMI_TIPOLOGIA_FILTRO"
static let SeismicSort = "EQN_SISMI_TIPOLOGIA_ORDINAMENTO"
static let SeismicMagnitudoMinima = "EQN_MAGNITUDO_MINIMA"
static let SeismicDistanzaMassima = "EQN_DISTANZA_MASSIMA"
static let SeismicEtaMassima = "EQN_ETA_MASSIMA"
static let SeismicSismiFortiAbilitati = "EQN_SISMI_FORTI_ABILITATI"
static let SeismicSismiForti = "EQN_SISMI_FORTI"
static let SeismicSismiQualsiasiMagnitudo = "EQN_SISMI_QUALSIASI_MAGNITUDO"
static let SeismicModificaImpostazioni = "EQN_SISMI_MODIFICA_IMPOSTAZIONI"
}
extension UserDefaults {
@@ -95,4 +101,13 @@ extension UserDefaults {
}
return defaultValue
}
func enumObject<T: RawRepresentable>(forKey key: String, or defaultValue: T) -> T {
if let rawValue = UserDefaults.standard.object(forKey: key) as? T.RawValue,
let value = T.init(rawValue: rawValue) {
return value
}
return defaultValue
}
}
@@ -26,8 +26,6 @@
@implementation AllerteViewController
static NSString * const SegueIdentifierPrioritySubscriptions = @"ShowPrioritySubscriptions";
/// Sections inside the app
typedef NS_ENUM(NSInteger, AllerteTableRow) {
AllerteTableRowLocationPermission = 0,
@@ -102,9 +100,18 @@ 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"];
[self.tableView registerClass:[AlertsPastEartquakesTableViewCell class] forCellReuseIdentifier:@"PastEarthquakesCell"];
[self.tableView registerClass:[AlertsSeismicNotificationCompactTableViewCell class] forCellReuseIdentifier:@"SeismicNotificationCompactCell"];
[self.tableView registerClass:[AlertsSeismicNotificationExpandedTableViewCell class] forCellReuseIdentifier:@"SeismicNotificationExpandedCell"];
[self.tableView registerClass:[AlertsPositionDataTableViewCell class] forCellReuseIdentifier:@"PositionDataCell"];
if (EQNBackgroundPositionDebugHelper.shared.isEnabled) {
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemBookmarks target:self action:@selector(backgroundPositionDebugTapped:)];
}
@@ -142,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)];
}
@@ -228,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)
@@ -265,14 +272,16 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
if (tableRow == AllerteTableRowLocationPermission) {
AlertsNoLocationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"NoLocationCell" forIndexPath:indexPath];
cell.status = CLLocationManager.authorizationStatus;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell updateWith:EQNUserData.sharedData.locationAuthorizationStatus];
return cell;
} else if (tableRow == AllerteTableRowSismiRilevati) {
if (self.isNotificaAttiva) {
AlertsSeismicNotificationExpandedTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SeismicNotificationExpandedCell" forIndexPath:indexPath];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
EQNRealtimePushNotification *notification = [EQNRealtimePushNotification storedNotification];
cell.notification = notification;
[cell updateWith:notification];
__weak AllerteViewController *weakSelf = self;
cell.onTapClose = ^{
@@ -291,7 +300,8 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
return cell;
}
AlertsSeismicNotificationCompactTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SeismicNotificationCell" forIndexPath:indexPath];
AlertsSeismicNotificationCompactTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SeismicNotificationCompactCell" forIndexPath:indexPath];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
__weak AllerteViewController *weakSelf = self;
cell.onTapAlertTest = ^{
@@ -311,8 +321,9 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
} else if (tableRow == AllerteTableRowAllertePassate) {
AlertsPastEartquakesTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PastEarthquakesCell" forIndexPath:indexPath];
cell.smartphoneNetwork = [EQNManager defaultManager].rete_smartphone;
cell.onTapMapButton = ^{
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell updateWith:[EQNManager defaultManager].rete_smartphone];
cell.onTapMap = ^{
PasquakesMapViewController *controller = [[PasquakesMapViewController alloc] init];
[self presentViewController:controller animated:YES completion:nil];
};
@@ -320,7 +331,8 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
} else if (tableRow == AllerteTableRowReteSmartphone) {
AlertsSmartphoneNetworkTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SmartphoneNetworkCell" forIndexPath:indexPath];
cell.smartphoneNetwork = [EQNManager defaultManager].rete_smartphone;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell updateWith:[EQNManager defaultManager].rete_smartphone];
cell.onTapButton = ^{
[self visualizzaCopertura];
};
@@ -328,12 +340,13 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
} else if (tableRow == AllerteTableRowServizioPriorita) {
AlertsPriorityServiceTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PriorityCell" forIndexPath:indexPath];
cell.smartphoneNetwork = [EQNManager defaultManager].rete_smartphone;
[cell updateWith:[EQNManager defaultManager].rete_smartphone];
return cell;
} else if (tableRow == AllerteTableRowDatiPosizione) {
AlertsPositionDataTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PositionDataCell" forIndexPath:indexPath];
cell.position = [EQNUser defaultUser].lastPosition;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell updateWith:[EQNUser defaultUser].lastPosition];
return cell;
}
@@ -346,9 +359,10 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
AllerteTableRow tableRow = [self.tableItems[indexPath.row] integerValue];
switch (tableRow) {
case AllerteTableRowServizioPriorita:
[self performSegueWithIdentifier:SegueIdentifierPrioritySubscriptions sender:nil];
break;
case AllerteTableRowServizioPriorita: {
SubscriptionsViewController *controller = [[SubscriptionsViewController alloc] init];
[self.navigationController pushViewController:controller animated:YES];
}; break;
default:
break;
}
@@ -9,41 +9,70 @@
import UIKit
import CoreLocation
class AlertsNoLocationTableViewCell: EQNBaseTableViewCell {
@objc var status: CLAuthorizationStatus = .notDetermined {
didSet {
updateUI()
}
@objc
class AlertsNoLocationTableViewCell: EQNBaseContainerTableViewCell {
override var isHeaderVisible: Bool { false }
// MARK: - UI
private lazy var messageLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.Colors.red
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private lazy var actionButton: UIButton = {
let button = EQNRoundedButton.make(title: NSLocalizedString("permission_location_no_background_solve", comment: ""), target: self, action: #selector(solveTapped(_:)))
return button
}()
// MARK: - Internal
override func setupUI() {
super.setupUI()
containerView.addSubview(messageLabel)
containerView.addSubview(actionButton)
messageLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
messageLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
messageLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
actionButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
actionButton.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: .cardPadding).isActive = true
actionButton.leadingAnchor.constraint(equalTo: messageLabel.leadingAnchor).isActive = true
actionButton.trailingAnchor.constraint(equalTo: messageLabel.trailingAnchor).isActive = true
actionButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
@IBOutlet private weak var messageLabel: UILabel!
@IBOutlet private weak var actionButton: UIButton!
override func updateUI() {
super.updateUI()
actionButton.backgroundColor = AppTheme.Colors.lightGray
}
// MARK: - Private
// MARK: - Public
private func updateUI() {
var message = ""
switch status {
case .authorizedAlways:
message = ""
case .authorizedWhenInUse:
message = NSLocalizedString("permission_location_no_background", comment: "")
default:
message = NSLocalizedString("permission_location_no", comment: "")
@objc
func update(with status: CLAuthorizationStatus) {
messageLabel.text = switch status {
case .authorizedAlways: ""
case .authorizedWhenInUse: NSLocalizedString("permission_location_no_background", comment: "")
default: NSLocalizedString("permission_location_no", comment: "")
}
messageLabel.text = message
actionButton.setLocalizedTitle(key: "permission_location_no_background_solve")
}
// MARK: - Actions
@IBAction private func solveTapped(_ sender: UIButton) {
@objc private func solveTapped(_ sender: UIButton) {
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
return
}
UIApplication.shared.open(settingsUrl, options: [:], completionHandler: nil)
}
}
@@ -8,50 +8,87 @@
import UIKit
class AlertsPastEartquakesTableViewCell: EQNBaseTableViewCell {
@objc var smartphoneNetwork: EQNReteSmartphone? {
didSet {
updateUI()
}
}
@objc
class AlertsPastEartquakesTableViewCell: EQNBaseContainerTableViewCell {
@objc var onTapMapButton: (() -> Void)?
@objc var onTapMap: (() -> Void)?
override var headerText: String { NSLocalizedString("main_past_quakes", comment: "") }
// MARK: - UI
private lazy var last24hLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .title3)
label.textAlignment = .center
return label
}()
private lazy var from2013Label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .title3)
label.textAlignment = .center
return label
}()
private lazy var mapButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(mapTapped(_:)))
return button
}()
// MARK: - Internal
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var last24hLabel: UILabel!
@IBOutlet private weak var from2013Label: UILabel!
@IBOutlet private weak var mapButton: UIButton!
// MARK: - View Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(last24hLabel)
containerView.addSubview(from2013Label)
containerView.addSubview(mapButton)
last24hLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
last24hLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
last24hLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
from2013Label.topAnchor.constraint(equalTo: last24hLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
from2013Label.leadingAnchor.constraint(equalTo: last24hLabel.leadingAnchor).isActive = true
from2013Label.trailingAnchor.constraint(equalTo: last24hLabel.trailingAnchor).isActive = true
mapButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
mapButton.topAnchor.constraint(equalTo: from2013Label.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
mapButton.leadingAnchor.constraint(equalTo: from2013Label.leadingAnchor).isActive = true
mapButton.trailingAnchor.constraint(equalTo: from2013Label.trailingAnchor).isActive = true
mapButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("main_past_quakes", comment: "")
override func updateUI() {
super.updateUI()
last24hLabel.text = NSLocalizedString("main_recent_quakes_initial", comment: "")
from2013Label.text = NSLocalizedString("main_total_quakes_initial", comment: "")
mapButton.setLocalizedTitle(key: "official_button_map", uppercased: true, emoji: "🗺")
}
private func updateUI() {
guard let smartphoneNetwork = smartphoneNetwork else { return }
// MARK: - Public
@objc
func update(with smartphoneNetwork: EQNReteSmartphone?) {
guard let smartphoneNetwork else { return }
last24hLabel.text = String(format: NSLocalizedString("main_recent_quakes", comment: ""), smartphoneNetwork.counterLastDayAlerts)
from2013Label.text = String(format: NSLocalizedString("main_total_quakes", comment: ""), smartphoneNetwork.counterTotalAlerts)
}
// MARK: - Actions
@IBAction func mapTapped(_ sender: UIButton) {
onTapMapButton?()
@objc private func mapTapped(_ sender: UIButton) {
onTapMap?()
}
}
@@ -9,20 +9,11 @@
import UIKit
import Solar
class AlertsPositionDataTableViewCell: EQNBaseTableViewCell {
@objc var position: CLLocation? {
didSet {
updateUI()
}
}
@objc
class AlertsPositionDataTableViewCell: EQNBaseContainerTableViewCell {
// MARK: - Internal
override var headerText: String { NSLocalizedString("weather_location", comment: "") }
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var positionLabel: UILabel!
@IBOutlet private weak var sunriseTimeLabel: UILabel!
@IBOutlet private weak var sunsetTimeLabel: UILabel!
private lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
@@ -30,26 +21,117 @@ class AlertsPositionDataTableViewCell: EQNBaseTableViewCell {
return formatter
}()
// MARK: - Private
// MARK: - UI
private func updateUI() {
headerLabel.text = NSLocalizedString("weather_location", comment: "")
private lazy var positionImage: UIImageView = {
let imageView = UIImageView(image: .init(named: "world_old"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
return imageView
}()
private lazy var positionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private lazy var sunriseImage: UIImageView = {
let imageView = UIImageView(image: .init(named: "sunrise"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
return imageView
}()
private lazy var sunriseTimeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private lazy var sunsetImage: UIImageView = {
let imageView = UIImageView(image: .init(named: "sunset"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
return imageView
}()
private lazy var sunsetTimeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
return label
}()
// MARK: - Internal
override func setupUI() {
super.setupUI()
containerView.addSubview(positionImage)
containerView.addSubview(positionLabel)
containerView.addSubview(sunriseImage)
containerView.addSubview(sunriseTimeLabel)
containerView.addSubview(sunsetImage)
containerView.addSubview(sunsetTimeLabel)
positionImage.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
positionImage.centerYAnchor.constraint(equalTo: positionLabel.centerYAnchor).isActive = true
positionImage.trailingAnchor.constraint(equalTo: positionLabel.leadingAnchor, constant: .cardPadding.negative).isActive = true
positionLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
positionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
sunriseImage.leadingAnchor.constraint(equalTo: positionImage.leadingAnchor).isActive = true
sunriseImage.centerYAnchor.constraint(equalTo: sunriseTimeLabel.centerYAnchor).isActive = true
sunriseImage.trailingAnchor.constraint(equalTo: sunriseTimeLabel.leadingAnchor, constant: .cardPadding.negative).isActive = true
sunriseTimeLabel.topAnchor.constraint(equalTo: positionLabel.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
sunriseTimeLabel.trailingAnchor.constraint(equalTo: positionLabel.trailingAnchor).isActive = true
sunsetImage.leadingAnchor.constraint(equalTo: sunriseImage.leadingAnchor).isActive = true
sunsetImage.centerYAnchor.constraint(equalTo: sunsetTimeLabel.centerYAnchor).isActive = true
sunsetImage.trailingAnchor.constraint(equalTo: sunsetTimeLabel.leadingAnchor, constant: .cardPadding.negative).isActive = true
sunsetTimeLabel.topAnchor.constraint(equalTo: sunriseTimeLabel.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
sunsetTimeLabel.trailingAnchor.constraint(equalTo: sunriseTimeLabel.trailingAnchor).isActive = true
sunsetTimeLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
override func updateUI() {
super.updateUI()
positionLabel.text = "n.d."
sunriseTimeLabel.text = "n.d."
sunsetTimeLabel.text = "n.d."
guard let position = position else { return }
}
// MARK: - Public
@objc
func update(with position: CLLocation?) {
guard let position else { return }
positionLabel.text = EQNUtility.coordinateString(coordinate: position.coordinate)
if let solar = Solar(coordinate: position.coordinate) {
let timeZone = TimeZone.current.localizedName(for: .generic, locale: .current) ?? TimeZone.current.identifier
if let sunrise = solar.sunrise {
sunriseTimeLabel.text = dateFormatter.string(from: sunrise) + " \(timeZone)"
}
if let sunset = solar.sunset {
sunsetTimeLabel.text = dateFormatter.string(from: sunset) + " \(timeZone)"
}
guard let solar = Solar(coordinate: position.coordinate) else { return }
let timeZone = TimeZone.current.localizedName(for: .generic, locale: .current) ?? TimeZone.current.identifier
if let sunrise = solar.sunrise {
sunriseTimeLabel.text = dateFormatter.string(from: sunrise) + " \(timeZone)"
}
if let sunset = solar.sunset {
sunsetTimeLabel.text = dateFormatter.string(from: sunset) + " \(timeZone)"
}
}
}
@@ -8,37 +8,62 @@
import UIKit
class AlertsPriorityServiceTableViewCell: EQNBaseTableViewCell {
@objc var smartphoneNetwork: EQNReteSmartphone? {
didSet {
updateUI()
}
}
@objc
class AlertsPriorityServiceTableViewCell: EQNBaseContainerTableViewCell {
override var headerText: String { NSLocalizedString("inapp_list", comment: "") }
override var isRightArrowVisbile: Bool { true }
// MARK: - UI
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.Colors.darkGray
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private lazy var lastSubscriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.Colors.pureRed
label.font = .preferredFont(forTextStyle: .body)
return label
}()
// MARK: - Internal
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var descriptionLabel: UILabel!
@IBOutlet private weak var lastSubscriptionLabel: UILabel!
// MARK: - View Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(descriptionLabel)
containerView.addSubview(lastSubscriptionLabel)
descriptionLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
lastSubscriptionLabel.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: .cardVerticalSpacing/2.0).isActive = true
lastSubscriptionLabel.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor).isActive = true
lastSubscriptionLabel.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
lastSubscriptionLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("inapp_list", comment: "")
override func updateUI() {
super.updateUI()
backgroundColor = AppTheme.Colors.cardBackgroundOrange
descriptionLabel.text = NSLocalizedString("inapp_adv", comment: "")
}
private func updateUI() {
guard let smartphoneNetwork = smartphoneNetwork else { return }
// MARK: - Public
@objc
func update(with smartphoneNetwork: EQNReteSmartphone?) {
guard let smartphoneNetwork else { return }
lastSubscriptionLabel.text = subscriptionText(for: smartphoneNetwork.lastSubscriptionDiff)
}
@@ -9,54 +9,112 @@
import UIKit
class AlertsSeismicNotificationCompactTableViewCell: EQNBaseTableViewCell {
@objc
class AlertsSeismicNotificationCompactTableViewCell: EQNBaseContainerTableViewCell {
typealias DefaultCompletion = () -> Void
@objc var onTapAlertTest: DefaultCompletion?
@objc var onTapSimulator: DefaultCompletion?
@objc var onTapHowItWorks: DefaultCompletion?
@objc var onTapShareApp: DefaultCompletion?
override var isHeaderVisible: Bool { false }
// MARK: - UI
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.Colors.green
label.font = .preferredFont(forTextStyle: .title3)
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
@IBOutlet private weak var descriptionLabel: UILabel!
@IBOutlet private weak var testAlertButton: UIButton!
@IBOutlet private weak var simulatorAlertButton: UIButton!
@IBOutlet private weak var howItWorksAlertButton: UIButton!
@IBOutlet private weak var shareAppButton: UIButton!
private lazy var testAlertButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(testAlertTapped(_:)))
return button
}()
// MARK: - View Lifecycle
private lazy var simulatorAlertButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(simulatorTapped(_:)))
return button
}()
override func awakeFromNib() {
super.awakeFromNib()
private lazy var howItWorksAlertButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(howItWorksTapped(_:)))
return button
}()
private lazy var shareAppButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(shareAppTapped(_:)))
return button
}()
// MARK: - Internal
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(descriptionLabel)
containerView.addSubview(testAlertButton)
containerView.addSubview(simulatorAlertButton)
containerView.addSubview(howItWorksAlertButton)
containerView.addSubview(shareAppButton)
descriptionLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardPadding).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
testAlertButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
simulatorAlertButton.heightAnchor.constraint(equalTo: testAlertButton.heightAnchor).isActive = true
howItWorksAlertButton.heightAnchor.constraint(equalTo: testAlertButton.heightAnchor).isActive = true
shareAppButton.heightAnchor.constraint(equalTo: testAlertButton.heightAnchor).isActive = true
testAlertButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
testAlertButton.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor).isActive = true
testAlertButton.trailingAnchor.constraint(equalTo: simulatorAlertButton.leadingAnchor, constant: .cardPadding.negative).isActive = true
simulatorAlertButton.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
simulatorAlertButton.centerYAnchor.constraint(equalTo: testAlertButton.centerYAnchor).isActive = true
simulatorAlertButton.widthAnchor.constraint(equalTo: testAlertButton.widthAnchor).isActive = true
howItWorksAlertButton.topAnchor.constraint(equalTo: testAlertButton.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
howItWorksAlertButton.leadingAnchor.constraint(equalTo: testAlertButton.leadingAnchor).isActive = true
howItWorksAlertButton.trailingAnchor.constraint(equalTo: shareAppButton.leadingAnchor, constant: .cardPadding.negative).isActive = true
shareAppButton.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
shareAppButton.centerYAnchor.constraint(equalTo: howItWorksAlertButton.centerYAnchor).isActive = true
shareAppButton.widthAnchor.constraint(equalTo: howItWorksAlertButton.widthAnchor).isActive = true
shareAppButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
override func updateUI() {
super.updateUI()
backgroundColor = AppTheme.Colors.cardBackgroundGreen
descriptionLabel.text = NSLocalizedString("main_nodetection", comment: "")
testAlertButton.setLocalizedTitle(key: "main_alerttest", uppercased: true, emoji: "🚨")
simulatorAlertButton.setLocalizedTitle(key: "main_simulator", uppercased: true, emoji: "")
howItWorksAlertButton.setLocalizedTitle(key: "main_how_it_work", uppercased: true, emoji: "💡")
shareAppButton.setLocalizedTitle(key: "main_share_app", uppercased: true, emoji: "👥")
}
// MARK: - Actions
@IBAction private func testAlertTapped() {
@objc private func testAlertTapped(_ sender: UIButton) {
onTapAlertTest?()
}
@IBAction private func simulatorTapped() {
@objc private func simulatorTapped(_ sender: UIButton) {
onTapSimulator?()
}
@IBAction private func howItWorksTapped() {
@objc private func howItWorksTapped(_ sender: UIButton) {
onTapHowItWorks?()
}
@IBAction private func shareAppTapped() {
@objc private func shareAppTapped(_ sender: UIButton) {
onTapShareApp?()
}
}
@@ -10,78 +10,173 @@ import UIKit
import MapKit
import Shogun
class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMapViewDelegate {
class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseContainerTableViewCell, MKMapViewDelegate {
override var isHeaderVisible: Bool { false }
typealias DefaultCompletion = () -> Void
@objc var notification: EQNRealtimePushNotification? {
didSet {
updateUI()
}
}
@objc var onTapOpenTwitter: DefaultCompletion?
@objc var onTapRateApp: DefaultCompletion?
@objc var onTapClose: DefaultCompletion?
@objc var onTapShareApp: DefaultCompletion?
// MARK: - UI
private lazy var notificationTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .title1)
label.textAlignment = .center
return label
}()
private lazy var notificationIntensityLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .title1)
label.textAlignment = .center
return label
}()
private lazy var notificationDescriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
return label
}()
private lazy var mapView: MKMapView = {
let mapView = MKMapView()
mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.delegate = self
mapView.isScrollEnabled = false
mapView.isZoomEnabled = false
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
return mapView
}()
private lazy var shareButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(shareAppTapped(_:)))
return button
}()
private lazy var rateAppButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(rateAppTapped(_:)))
return button
}()
private lazy var viewOnTwitterButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(viewInTwitterTapped(_:)))
return button
}()
private lazy var closeButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(closeTapped(_:)))
return button
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
return label
}()
// MARK: - Internal
@IBOutlet weak var notificationTitleLabel: UILabel!
@IBOutlet weak var notificationDescriptionLabel: UILabel!
@IBOutlet weak var notificationIntensityLabel: UILabel!
@IBOutlet weak var waveTimeLabel: UILabel!
@IBOutlet weak var mapView: MKMapView! {
didSet {
mapView.delegate = self
mapView.isScrollEnabled = false
mapView.isZoomEnabled = false
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
}
}
@IBOutlet private weak var shareButton: UIButton!
@IBOutlet private weak var rateAppButton: UIButton!
@IBOutlet private weak var viewOnTwitterButton: UIButton!
@IBOutlet private weak var closeButton: UIButton!
@IBOutlet private weak var descriptionLabel: UILabel!
private var impactTimestamp: Date?
private var countdownTimer: Timer?
// MARK: - View Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
setUI()
let stackView = UIStackView(arrangedSubviews: [shareButton, rateAppButton])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.spacing = .cardVerticalSpacing
containerView.addSubview(notificationTitleLabel)
containerView.addSubview(notificationIntensityLabel)
containerView.addSubview(notificationDescriptionLabel)
containerView.addSubview(mapView)
containerView.addSubview(stackView)
containerView.addSubview(viewOnTwitterButton)
containerView.addSubview(descriptionLabel)
containerView.addSubview(closeButton)
notificationTitleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: .cardPadding).isActive = true
notificationTitleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
notificationTitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
notificationIntensityLabel.topAnchor.constraint(equalTo: notificationTitleLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
notificationIntensityLabel.leadingAnchor.constraint(equalTo: notificationTitleLabel.leadingAnchor).isActive = true
notificationIntensityLabel.trailingAnchor.constraint(equalTo: notificationTitleLabel.trailingAnchor).isActive = true
notificationDescriptionLabel.topAnchor.constraint(equalTo: notificationIntensityLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
notificationDescriptionLabel.leadingAnchor.constraint(equalTo: notificationTitleLabel.leadingAnchor).isActive = true
notificationDescriptionLabel.trailingAnchor.constraint(equalTo: notificationTitleLabel.trailingAnchor).isActive = true
mapView.topAnchor.constraint(equalTo: notificationDescriptionLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
mapView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
mapView.heightAnchor.constraint(greaterThanOrEqualToConstant: 240.0).isActive = true
shareButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
rateAppButton.heightAnchor.constraint(equalTo: shareButton.heightAnchor).isActive = true
viewOnTwitterButton.heightAnchor.constraint(equalTo: shareButton.heightAnchor).isActive = true
closeButton.heightAnchor.constraint(equalTo: shareButton.heightAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: mapView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
stackView.leadingAnchor.constraint(equalTo: notificationDescriptionLabel.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: notificationDescriptionLabel.trailingAnchor).isActive = true
viewOnTwitterButton.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
viewOnTwitterButton.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
viewOnTwitterButton.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true
descriptionLabel.topAnchor.constraint(equalTo: viewOnTwitterButton.bottomAnchor, constant: .cardPadding).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: viewOnTwitterButton.leadingAnchor).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: viewOnTwitterButton.trailingAnchor).isActive = true
closeButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
closeButton.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor).isActive = true
closeButton.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
closeButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardVerticalSpacing.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
override func updateUI() {
super.updateUI()
shareButton.setLocalizedTitle(key: "main_share_app")
rateAppButton.setLocalizedTitle(key: "main_vote")
viewOnTwitterButton.setLocalizedTitle(key: "main_twitter_see")
closeButton.setLocalizedTitle(key: "official_close")
descriptionLabel.text = NSLocalizedString("map_smartphone_magnitude", comment: "")
}
// MARK: - Public
private func setUI() {
shareButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
rateAppButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
viewOnTwitterButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
closeButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
}
private func updateUI() {
@objc
func update(with notification: EQNRealtimePushNotification?) {
// clearn any other previous notifications
notificationTitleLabel.text = ""
notificationDescriptionLabel.text = ""
notificationTitleLabel.text = "Sisma rilevato a 150km (TEST)"
notificationIntensityLabel.text = "Previsto uno scuotimento forte"
notificationDescriptionLabel.text = "Distanza 150 km - 13 minuti fa"
mapView.removeAnnotations(mapView.annotations)
guard let notification = notification else { return }
containerView.backgroundColor = backgroundColor(for: notification.relativeIntensity())
backgroundColor = backgroundColor(for: notification.relativeIntensity())
notificationTitleLabel.text = notification.title
notificationIntensityLabel.text = notification.displayBody
notificationIntensityLabel.textColor = notification.relativeIntensityColor
@@ -95,7 +190,6 @@ class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMa
notificationDescriptionLabel.text = ""
+ NSLocalizedString("official_card_distance", comment: "") + " \(distanceRound) km"
+ " - " + EQNUtility.formattedString(forTimeDifference: difference)
+ "\n" + String(format: NSLocalizedString("map_number", comment: ""), "\(notification.counter)")
}
let span = MKCoordinateSpan(latitudeDelta: 10.5, longitudeDelta: 10.5)
@@ -118,37 +212,37 @@ class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMa
annotationView.title = annotation.title
return annotationView
}
// MARK: - Actions
@IBAction private func shareAppTapped(_ sender: UIButton) {
@objc private func shareAppTapped(_ sender: UIButton) {
onTapShareApp?()
}
@IBAction private func rateAppTapped(_ sender: UIButton) {
@objc private func rateAppTapped(_ sender: UIButton) {
onTapRateApp?()
}
@IBAction private func viewInTwitterTapped(_ sender: UIButton) {
@objc private func viewInTwitterTapped(_ sender: UIButton) {
onTapOpenTwitter?()
}
@IBAction private func closeTapped(_ sender: UIButton) {
@objc private func closeTapped(_ sender: UIButton) {
onTapClose?()
}
// MARK: - Helpers
// MARK: - Private
private func backgroundColor(for intensity: Double) -> UIColor {
switch intensity {
case _ where intensity < 0.004:
return UIColor(named: "Gray (card background)")!
return AppTheme.Colors.cardBackgroundGray
case _ where intensity < 0.30:
return UIColor(named: "Green (card background)")!
return AppTheme.Colors.cardBackgroundGreen
case _ where intensity < 0.70:
return UIColor(named: "Yellow (card background)")!
return AppTheme.Colors.cardBackgroundYellow
default:
return UIColor(named: "Red (card background)")!
return AppTheme.Colors.cardBackgroundRed
}
}
}
@@ -7,49 +7,83 @@
//
import UIKit
import Shogun
class AlertsSmartphoneNetworkTableViewCell: EQNBaseTableViewCell {
@objc var smartphoneNetwork: EQNReteSmartphone? {
didSet {
updateUI()
}
}
@objc
class AlertsSmartphoneNetworkTableViewCell: EQNBaseContainerTableViewCell {
@objc var onTapButton: (() -> Void)?
override var headerText: String { NSLocalizedString("main_network", comment: "") }
// MARK: - UI
private lazy var counterLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.Colors.green
label.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
label.textAlignment = .center
return label
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
return label
}()
private lazy var coverageButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(localCovergeTapped(_:)))
return button
}()
// MARK: - Internal
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var smartphoneCounterLabel: UILabel!
@IBOutlet private weak var coverageDescriptionLabel: UILabel!
@IBOutlet private weak var localCoverageButton: UIButton!
// MARK: - View Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(counterLabel)
containerView.addSubview(descriptionLabel)
containerView.addSubview(coverageButton)
counterLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
counterLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
counterLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
descriptionLabel.topAnchor.constraint(equalTo: counterLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
coverageButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
coverageButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
coverageButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
coverageButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
coverageButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("main_network", comment: "")
coverageDescriptionLabel.text = NSLocalizedString("main_monitoring_currently2", comment: "")
localCoverageButton.setLocalizedTitle(key: "main_coverage", uppercased: true, emoji: "🗺")
override func updateUI() {
super.updateUI()
coverageButton.setLocalizedTitle(key: "main_coverage", uppercased: true, emoji: "🗺")
descriptionLabel.text = NSLocalizedString("main_monitoring_currently2", comment: "")
}
private func updateUI() {
guard let smartphoneNetwork = smartphoneNetwork else { return }
smartphoneCounterLabel.text = "\(smartphoneNetwork.counterSmartphones)"
// MARK: - Public
@objc
func update(with smartphoneNetwork: EQNReteSmartphone?) {
guard let smartphoneNetwork else { return }
counterLabel.text = "\(smartphoneNetwork.counterSmartphones)"
}
// MARK: - Actions
@IBAction private func localCovergeTapped(_ sender: UIButton) {
@objc private func localCovergeTapped(_ sender: UIButton) {
onTapButton?()
}
}
@@ -98,8 +98,8 @@ class PasquakesMapViewController: EQNBaseMapViewController {
.first
// controlliamo che sia inferiore al raggio massimo impostato per le notifiche
if let radiusLow = Double(EQNAllertaSismica.shared().raggioSismiLievi),
let radiusStrong = Double(EQNAllertaSismica.shared().raggioSismiForti),
if let radiusLow = Double(EQNSettingRealTimeAlert.shared.raggioSismiLievi),
let radiusStrong = Double(EQNSettingRealTimeAlert.shared.raggioSismiForti),
let nearestPastquake = nearestPastquake {
let radius = max(radiusLow, radiusStrong)
if abs(nearestPastquake.coordinate.distance(from: userPosition)) < radius {
@@ -9,7 +9,6 @@
#import "EQNMainTabBarController.h"
#import "AppDelegate.h"
#import "EQNBaseViewController.h"
#import "SettingsBaseViewController.h"
#import "EQNManager.h"
#import "ServerRequest.h"
@@ -20,9 +19,6 @@
@implementation EQNMainTabBarController
static NSString * const SegueIdentifierSettings = @"ShowSettings";
static NSString * const SegueIdentifierLogs = @"ShowLogs";
#pragma mark - View Lifecycle
- (void)viewDidLoad
@@ -41,6 +37,7 @@ static NSString * const SegueIdentifierLogs = @"ShowLogs";
object:nil];
[self sincronizza];
[self migrationV5_8];
}
#pragma mark - Private
@@ -53,6 +50,23 @@ static NSString * const SegueIdentifierLogs = @"ShowLogs";
self.tabBar.items[EQNTabBarSectionImpostazioni].title = [NSLocalizedString(@"drawer_main_settings", comment: "") capitalizedString];
}
- (void)migrationV5_8
{
// forziamo il salvataggio delle impostazioni di notifica, perchè i vari valori devono essere migrati
BOOL alreadyMigrated = [NSUserDefaults.standardUserDefaults boolForKey:NSUserDefaults.SaveSettingsNotificationMigrationV5_8];
if (alreadyMigrated) {
return;
}
NSLog(@"[MIGRATION] perform notification settings save");
[SettingsBaseTableViewController saveSettingsWithCompletion:^(BOOL success) {
if (success) {
NSLog(@"[MIGRATION] settings saved");
[NSUserDefaults.standardUserDefaults setBool:true forKey:NSUserDefaults.SaveSettingsNotificationMigrationV5_8];
}
}];
}
#pragma mark - Notification
- (void)serverRegistrationFailedNotification:(NSNotification *)notification
@@ -119,7 +133,7 @@ static NSString * const SegueIdentifierLogs = @"ShowLogs";
// if user switch from settings page, we need to force a settings save
UIViewController *controller = [self getTopControllerFromController:tabBarController.selectedViewController];
if ([controller isKindOfClass:[SettingsViewController class]]) {
[SettingsBaseViewController saveSettings];
[SettingsBaseTableViewController saveSettings];
}
return YES;
@@ -1,151 +0,0 @@
//
// SubscriptionDetailViewController.swift
// Earthquake Network
//
// Created by Busi Andrea on 29/07/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
import UIKit
import SafariServices
import StoreKit
class SubscriptionDetailViewController: UIViewController {
/// Enable this allows shake to enable the current subscription
private static let ShakeToEnableSubscription = false
var product: SKProduct? {
didSet {
updateUI()
}
}
@IBOutlet private weak var containerView: UIView!
@IBOutlet private weak var productTitleLabel: UILabel!
@IBOutlet private weak var productImageView: UIImageView!
@IBOutlet private weak var productDescriptionLabel: UILabel!
@IBOutlet private weak var subscriptionDetailsLabel: UILabel!
@IBOutlet private weak var openPrivacyButton: UIButton!
@IBOutlet private weak var openTermsButton: UIButton!
@IBOutlet private weak var purchaseRecapLabel: UILabel!
@IBOutlet private weak var productPriceLabel: UILabel!
@IBOutlet private weak var purchaseButton: UIButton!
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(handlePurchaseNotification(_:)),
name: .EQNInAppPurchaseDidComplete,
object: nil)
updateUI()
setupUI()
}
// MARK: - Private
private func setupUI() {
containerView.eqn_applyShadowAndRoundedCorners()
}
private func updateUI() {
guard let product = product, isViewLoaded else { return }
productImageView.image = VersioneProProducts.image(for: product.productIdentifier)
productTitleLabel.text = product.localizedTitle
productDescriptionLabel.text = product.localizedDescription
var purchaseRecapString = ""
var subscriptionDetailsString = ""
switch product.productIdentifier {
case VersioneProProducts.Identifier.Subscription10kMonthly,
VersioneProProducts.Identifier.Subscription100kMonthly:
purchaseRecapString = "inapp_monthly_payment"
subscriptionDetailsString = "inapp_detail_description"
case VersioneProProducts.Identifier.Subscription100kYearly,
VersioneProProducts.Identifier.Subscription100kYearlyDiscounted,
VersioneProProducts.Identifier.Subscription10kYearly,
VersioneProProducts.Identifier.Subscription10kYearlyDiscounted:
purchaseRecapString = "inapp_yearly_payment"
subscriptionDetailsString = "inapp_detail_description"
case VersioneProProducts.Identifier.Subscription10kPerpetual,
VersioneProProducts.Identifier.Subscription100kPerpetual:
purchaseRecapString = "inapp_lifetime_payment"
subscriptionDetailsString = "inapp_lifetime_detail_description"
default:
break
}
subscriptionDetailsLabel.text = NSLocalizedString(subscriptionDetailsString, comment: "")
openPrivacyButton.setTitle(NSLocalizedString("network_pro_privacy_disclaimer", comment: ""), for: .normal)
openTermsButton.setTitle(NSLocalizedString("network_pro_terms_conditions", comment: ""), for: .normal)
purchaseRecapLabel.text = "\(product.localizedDescription), \(NSLocalizedString(purchaseRecapString, comment: ""))"
priceFormatter.locale = product.priceLocale
productPriceLabel.text = priceFormatter.string(from: product.price)
purchaseButton.setTitle(NSLocalizedString("inapp_purchase", comment: ""), for: .normal)
}
// MARK: - Notifications
@objc func handlePurchaseNotification(_ notification: Notification) {
navigationController?.popViewController(animated: true)
}
// MARK: - Actions
@IBAction func openExternalLinkTapped(_ sender: UIButton) {
var linkUrl: URL?
if sender == openPrivacyButton {
linkUrl = URL(string: "\(EQNWebsiteAddress)/privacy/")
} else if sender == openTermsButton {
linkUrl = URL(string: "\(EQNWebsiteAddress)/terms-conditions/")
}
if let url = linkUrl {
let controller = SFSafariViewController(url: url)
present(controller, animated: true, completion: nil)
}
}
@IBAction func subscribeTapped(_ sender: UIButton) {
guard let product = product else { return }
VersioneProProducts.store.buyProduct(product)
}
// MARK: - Helper
private var priceFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.formatterBehavior = .behavior10_4
formatter.numberStyle = .currency
return formatter
}()
}
extension SubscriptionDetailViewController {
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
guard let product = product, event?.subtype == .motionShake, Self.ShakeToEnableSubscription else {
return
}
let alert = UIAlertController(title: "🧑‍💻", message: "Please select an action", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Reset all purchases", style: .default) { action in
EQNPurchaseUtility.resetInAppPurchases()
})
alert.addAction(UIAlertAction(title: "Activate this subscription", style: .default) { action in
EQNPurchaseUtility.simulateProPurchase(identifier: product.productIdentifier)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
}
@@ -0,0 +1,204 @@
//
// SubscriptionDetailsTableViewCell.swift
// Earthquake Network
//
// Created by Andrea Busi on 18/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import UIKit
class SubscriptionDetailsTableViewCell: EQNBaseContainerTableViewCell {
var onTapPrivacy: () -> Void = { }
var onTapTerms: () -> Void = { }
var onTapPurchase: () -> Void = { }
var onChangePlan: (_ type: EQNInAppProducts.Plan) -> Void = { _ in }
override var isHeaderVisible: Bool { false }
// MARK: - UI
lazy var planSegmentedControl: UISegmentedControl = {
let control = UISegmentedControl(items: EQNInAppProducts.Plan.allCases.map(\.localizedTitle))
control.translatesAutoresizingMaskIntoConstraints = false
control.addTarget(self, action: #selector(onChangeSegmentedControl(_:)), for: .valueChanged)
return control
}()
lazy var productTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .title1)
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
lazy var productImageView: UIImageView = {
let imageView = UIImageView(image: .init(named: "top_100k"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.heightAnchor.constraint(greaterThanOrEqualToConstant: 50.0).isActive = true
return imageView
}()
lazy var subscriptionDetailsLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .justified
label.numberOfLines = 0
return label
}()
lazy var openPrivacyButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(onTapOpenPrivacyButton(_:)), for: .touchUpInside)
button.contentHorizontalAlignment = .leading
return button
}()
lazy var openTermsButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(onTapOpenTermsButton(_:)), for: .touchUpInside)
button.contentHorizontalAlignment = .leading
return button
}()
lazy var purchaseRecapLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .headline)
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
lazy var productPriceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .largeTitle)
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
lazy var purchaseButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(onTapPurchaseButton(_:)), for: .touchUpInside)
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
button.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
button.backgroundColor = .systemGroupedBackground
button.eqn_applyShadowAndRoundedCorners()
return button
}()
// MARK: - Internal
override func setupUI() {
super.setupUI()
containerView.addSubview(planSegmentedControl)
containerView.addSubview(productTitleLabel)
containerView.addSubview(productImageView)
containerView.addSubview(subscriptionDetailsLabel)
containerView.addSubview(openPrivacyButton)
containerView.addSubview(openTermsButton)
containerView.addSubview(purchaseRecapLabel)
containerView.addSubview(productPriceLabel)
containerView.addSubview(purchaseButton)
let leading: NSLayoutXAxisAnchor = planSegmentedControl.leadingAnchor
let trailing: NSLayoutXAxisAnchor = planSegmentedControl.trailingAnchor
planSegmentedControl.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
planSegmentedControl.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
planSegmentedControl.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
productTitleLabel.topAnchor.constraint(equalTo: planSegmentedControl.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
productTitleLabel.leadingAnchor.constraint(equalTo: leading, constant: .cardPadding).isActive = true
productTitleLabel.trailingAnchor.constraint(equalTo: trailing, constant: .cardPadding.negative).isActive = true
productImageView.topAnchor.constraint(equalTo: productTitleLabel.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
productImageView.leadingAnchor.constraint(equalTo: leading).isActive = true
productImageView.trailingAnchor.constraint(equalTo: trailing).isActive = true
purchaseRecapLabel.topAnchor.constraint(equalTo: productImageView.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
purchaseRecapLabel.leadingAnchor.constraint(equalTo: leading).isActive = true
purchaseRecapLabel.trailingAnchor.constraint(equalTo: trailing).isActive = true
productPriceLabel.topAnchor.constraint(equalTo: purchaseRecapLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
productPriceLabel.leadingAnchor.constraint(equalTo: leading).isActive = true
productPriceLabel.trailingAnchor.constraint(equalTo: trailing).isActive = true
purchaseButton.topAnchor.constraint(equalTo: productPriceLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
purchaseButton.leadingAnchor.constraint(equalTo: leading).isActive = true
purchaseButton.trailingAnchor.constraint(equalTo: trailing).isActive = true
subscriptionDetailsLabel.topAnchor.constraint(equalTo: purchaseButton.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
subscriptionDetailsLabel.leadingAnchor.constraint(equalTo: leading).isActive = true
subscriptionDetailsLabel.trailingAnchor.constraint(equalTo: trailing).isActive = true
openPrivacyButton.topAnchor.constraint(equalTo: subscriptionDetailsLabel.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
openPrivacyButton.leadingAnchor.constraint(equalTo: leading).isActive = true
openPrivacyButton.trailingAnchor.constraint(equalTo: trailing).isActive = true
openTermsButton.topAnchor.constraint(equalTo: openPrivacyButton.bottomAnchor, constant: .cardPadding).isActive = true
openTermsButton.leadingAnchor.constraint(equalTo: leading).isActive = true
openTermsButton.trailingAnchor.constraint(equalTo: trailing).isActive = true
openTermsButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardVerticalSpacing.x2.negative).isActive = true
}
override func updateUI() {
super.updateUI()
openPrivacyButton.setTitle(NSLocalizedString("network_pro_privacy_disclaimer", comment: ""), for: .normal)
openTermsButton.setTitle(NSLocalizedString("network_pro_terms_conditions", comment: ""), for: .normal)
purchaseButton.setTitle(NSLocalizedString("inapp_purchase", comment: ""), for: .normal)
}
// MARK: - Actions
@objc private func onTapOpenPrivacyButton(_ sender: UIButton) {
onTapPrivacy()
}
@objc private func onTapOpenTermsButton(_ sender: UIButton) {
onTapTerms()
}
@objc private func onTapPurchaseButton(_ sender: UIButton) {
onTapPurchase()
}
@objc private func onChangeSegmentedControl(_ sender: UISegmentedControl) {
let type: EQNInAppProducts.Plan = .from(index: sender.selectedSegmentIndex)
onChangePlan(type)
}
}
extension EQNInAppProducts.Plan {
var index: Int {
switch self {
case .monthly: 0
case .yearly: 1
case .perpetual: 2
}
}
static func from(index: Int) -> Self {
switch index {
case 0: .monthly
case 1: .yearly
default: .perpetual
}
}
}
@@ -0,0 +1,183 @@
//
// SubscriptionDetailsViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 18/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import UIKit
import StoreKit
import SafariServices
import Shogun
class SubscriptionDetailsViewController: UITableViewController {
/// Enable this allows shake to enable the current subscription
private static let ShakeToEnableSubscription = false
// MARK: - Internal
private let products: [EQNInAppProducts]
private var selectedProduct: EQNInAppProducts {
didSet {
onProductSelected()
}
}
private var priceFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.formatterBehavior = .behavior10_4
formatter.numberStyle = .currency
return formatter
}()
// MARK: - Init
init(
products: [EQNInAppProducts]
) {
self.products = products
self.selectedProduct = products.first(where: { $0.plan == .yearly }) ?? products.first!
super.init(style: .plain)
}
required init?(coder: NSCoder) {
fatalError("Please use init(products:) instead.")
}
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
addObservers()
}
private func addObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(handlePurchaseNotification(_:)),
name: .EQNInAppPurchaseDidComplete,
object: nil)
}
private func configureUI() {
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 2000.0
tableView.separatorStyle = .none
tableView.backgroundColor = .systemGroupedBackground
tableView.contentInset = EQNBaseContainerTableViewCell.EdgeInsets
tableView.registerCell(for: SubscriptionDetailsTableViewCell.self)
}
// MARK: - Notifications
@objc private func handlePurchaseNotification(_ notification: Notification) {
navigationController?.popViewController(animated: true)
}
// MARK: - Table view delegate & data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(cellIdentifiable: SubscriptionDetailsTableViewCell.self, for: indexPath)
cell.selectionStyle = .none
cell.productTitleLabel.text = selectedProduct.product.localizedTitle
cell.productImageView.image = selectedProduct.category.image
var purchaseRecapString = ""
var subscriptionDetailsString = ""
switch selectedProduct.productIdentifier {
case EQNInAppProducts.Identifier.Subscription10kMonthly,
EQNInAppProducts.Identifier.Subscription100kMonthly:
purchaseRecapString = "inapp_monthly_payment"
subscriptionDetailsString = "inapp_detail_description"
case EQNInAppProducts.Identifier.Subscription100kYearly,
EQNInAppProducts.Identifier.Subscription100kYearlyDiscounted,
EQNInAppProducts.Identifier.Subscription10kYearly,
EQNInAppProducts.Identifier.Subscription10kYearlyDiscounted:
purchaseRecapString = "inapp_yearly_payment"
subscriptionDetailsString = "inapp_detail_description"
case EQNInAppProducts.Identifier.Subscription10kPerpetual,
EQNInAppProducts.Identifier.Subscription100kPerpetual:
purchaseRecapString = "inapp_lifetime_payment"
subscriptionDetailsString = "inapp_lifetime_detail_description"
default:
break
}
cell.subscriptionDetailsLabel.text = NSLocalizedString(subscriptionDetailsString, comment: "")
cell.onTapPrivacy = { [weak self] in
self?.openExternalLink("\(EQNWebsiteAddress)/privacy/")
}
cell.onTapTerms = { [weak self] in
self?.openExternalLink("\(EQNWebsiteAddress)/terms-conditions/")
}
cell.onTapPurchase = { [weak self] in
self?.purchaseSelectedProduct()
}
cell.onChangePlan = { [weak self] type in
if let product = self?.productFromProductType(type) {
self?.selectedProduct = product
}
}
cell.planSegmentedControl.selectedSegmentIndex = selectedProduct.plan.index
cell.purchaseRecapLabel.text = "\(selectedProduct.product.localizedDescription), \(NSLocalizedString(purchaseRecapString, comment: ""))"
cell.productPriceLabel.text = priceFormatter.string(from: selectedProduct.product.price)
return cell
}
// MARK: - Private
private func onProductSelected() {
priceFormatter.locale = selectedProduct.product.priceLocale
tableView.reloadData()
}
private func openExternalLink(_ stringUrl: String) {
if let url = URL(string: stringUrl) {
let controller = SFSafariViewController(url: url)
present(controller, animated: true, completion: nil)
}
}
private func purchaseSelectedProduct() {
EQNInAppProducts.store.buyProduct(selectedProduct.product)
}
private func productFromProductType(_ type: EQNInAppProducts.Plan) -> EQNInAppProducts? {
let product: EQNInAppProducts?
switch type {
case .monthly:
product = products.first { $0.plan == .monthly }
case .yearly:
product = products.first { $0.plan == .yearly }
case .perpetual:
product = products.first { $0.plan == .perpetual }
}
return product
}
}
extension SubscriptionDetailsViewController {
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
guard event?.subtype == .motionShake, Self.ShakeToEnableSubscription else {
return
}
let alert = UIAlertController(title: "🧑‍💻", message: "Please select an action", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Reset all purchases", style: .default) { action in
EQNPurchaseUtility.resetInAppPurchases()
})
alert.addAction(UIAlertAction(title: "Activate this subscription", style: .default) { action in
EQNPurchaseUtility.simulateProPurchase(identifier: self.selectedProduct.productIdentifier)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
}
@@ -9,57 +9,81 @@
import UIKit
import StoreKit
class SubscriptionProductTableViewCell: UITableViewCell {
class SubscriptionProductTableViewCell: EQNBaseContainerTableViewCell {
override var isHeaderVisible: Bool { false }
override var isRightArrowVisbile: Bool { true }
// MARK: - UI
private lazy var productImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
return imageView
}()
var product: SKProduct? {
didSet {
updateUI()
}
}
var availability: EQNPurchaseAvailability? {
didSet {
updateUI()
}
}
private lazy var productTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .headline)
label.numberOfLines = 0
return label
}()
@IBOutlet private weak var productImageView: UIImageView!
@IBOutlet private weak var productTitleLabel: UILabel!
@IBOutlet private weak var productDescriptionLabel: UILabel?
@IBOutlet private weak var productInfoLabel: UILabel!
// MARK: - View Lifecycle
// force an inset to have the same style of EQNBaseTableViewCell
override var frame: CGRect {
get {
return super.frame
}
set (newFrame) {
let inset: CGFloat = 8
var frame = newFrame
frame.origin.x += inset
frame.size.width -= 2 * inset
super.frame = frame
}
}
// MARK: - Private
private func updateUI() {
guard let product = product else { return }
private lazy var productInfoLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.Colors.red
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 0
return label
}()
productImageView.image = VersioneProProducts.image(for: product.productIdentifier)
productTitleLabel.text = product.localizedTitle
productDescriptionLabel?.text = product.localizedDescription
// MARK: - Internal
override func setupUI() {
super.setupUI()
let infoKey = VersioneProProducts.is100kSubscription(for: product.productIdentifier) ? "inapp_available_100k" : "inapp_available_10k"
let counter = availability(for: product.productIdentifier)
containerView.addSubview(productImageView)
containerView.addSubview(productTitleLabel)
containerView.addSubview(productInfoLabel)
productImageView.widthAnchor.constraint(equalToConstant: 80.0).isActive = true
productImageView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
productTitleLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
productTitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
productImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
productImageView.trailingAnchor.constraint(equalTo: productTitleLabel.leadingAnchor, constant: .cardPadding.negative).isActive = true
productImageView.centerYAnchor.constraint(equalTo: productTitleLabel.centerYAnchor).isActive = true
productInfoLabel.topAnchor.constraint(equalTo: productTitleLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
productInfoLabel.leadingAnchor.constraint(equalTo: productImageView.leadingAnchor).isActive = true
productInfoLabel.trailingAnchor.constraint(equalTo: productTitleLabel.trailingAnchor).isActive = true
productInfoLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardVerticalSpacing.negative).isActive = true
}
// MARK: - Public
func update(
category: EQNInAppProducts.Category,
availability: EQNPurchaseAvailability?
) {
productImageView.image = category.image
productTitleLabel.text = category.localizedTitle
let infoKey = category == .top100k ? "inapp_available_100k" : "inapp_available_10k"
let counter = availabilityCounter(for: category, availability: availability)
productInfoLabel.text = String(format: NSLocalizedString(infoKey, comment: ""), counter)
}
private func availability(for productIdentifier: String) -> Int {
if VersioneProProducts.is100kSubscription(for: productIdentifier) {
private func availabilityCounter(
for category: EQNInAppProducts.Category,
availability: EQNPurchaseAvailability?
) -> Int {
if category == .top100k {
return availability?.top100kAvailable ?? 0
}
return availability?.top10kAvailable ?? 0
@@ -9,38 +9,87 @@
import UIKit
import StoreKit
class SubscriptionsActiveTableViewCell: EQNBaseTableViewCell {
var product: SKProduct? {
didSet {
updateUI()
}
}
class SubscriptionsActiveTableViewCell: EQNBaseContainerTableViewCell {
override var headerText: String { NSLocalizedString("inapp_active", comment: "") }
var onTapRestore: () -> Void = { }
// MARK: - UI
private lazy var noSubscriptionsLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var noSubscriptionsLabel: UILabel!
@IBOutlet private weak var activeSubscriptionImageView: UIImageView!
private lazy var activeSubscriptionImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
return imageView
}()
// MARK: - View Lifecycle
private lazy var restoreButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(restoreSubscriptionsTapped(_:)), for: .touchUpInside)
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
button.backgroundColor = .systemGroupedBackground
button.eqn_applyShadowAndRoundedCorners()
return button
}()
// MARK: - Internal
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
let stackView = UIStackView(arrangedSubviews: [ activeSubscriptionImageView, noSubscriptionsLabel, restoreButton ])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.alignment = .center
stackView.distribution = .equalSpacing
stackView.axis = .vertical
stackView.spacing = 20.0
containerView.addSubview(stackView)
activeSubscriptionImageView.widthAnchor.constraint(equalToConstant: 150.0).isActive = true
activeSubscriptionImageView.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
restoreButton.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
restoreButton.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
restoreButton.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
stackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardVerticalSpacing.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("inapp_active", comment: "")
override func updateUI() {
super.updateUI()
noSubscriptionsLabel.text = NSLocalizedString("inapp_nosub", comment: "")
restoreButton.setTitle(NSLocalizedString("purchase_pro_restore", comment: ""), for: .normal)
}
private func updateUI() {
if let productIdentifier = product?.productIdentifier {
// MARK: - Actions
@objc private func restoreSubscriptionsTapped(_ sender: UIButton) {
onTapRestore()
}
// MARK: - Public
func update(with product: EQNInAppProducts?) {
if let product {
noSubscriptionsLabel.isHidden = true
activeSubscriptionImageView.isHidden = false
activeSubscriptionImageView.image = VersioneProProducts.image(for: productIdentifier)
activeSubscriptionImageView.image = product.category.image
} else {
noSubscriptionsLabel.isHidden = false
activeSubscriptionImageView.isHidden = true
@@ -8,21 +8,40 @@
import UIKit
class SubscriptionsDescriptionTableViewCell: EQNBaseTableViewCell {
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var descriptionLabel: UILabel!
class SubscriptionsDescriptionTableViewCell: EQNBaseContainerTableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
override var headerText: String { NSLocalizedString("inapp_list", comment: "") }
// MARK: - UI
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 0
label.textAlignment = .justified
return label
}()
// MARK: - Internal
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(descriptionLabel)
descriptionLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
descriptionLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardVerticalSpacing.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("inapp_list", comment: "")
override func updateUI() {
super.updateUI()
descriptionLabel.text = NSLocalizedString("inapp_description", comment: "")
}
}
@@ -8,26 +8,55 @@
import UIKit
class SubscriptionsHeaderTableViewCell: UITableViewCell {
var isLoading = false {
didSet {
updateUI()
}
class SubscriptionsHeaderTableViewCell: UITableViewHeaderFooterView {
// MARK: - UI
private lazy var headerTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.preferredFont(forTextStyle: .title2)
label.textColor = AppTheme.Colors.darkGray
label.textAlignment = .center
return label
}()
private lazy var loadingActivityIndicator: UIActivityIndicatorView = {
let spinner = UIActivityIndicatorView(style: .medium)
spinner.translatesAutoresizingMaskIntoConstraints = false
spinner.hidesWhenStopped = true
return spinner
}()
// MARK: - Init
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
setupUI()
}
var title: String? = nil {
didSet {
updateUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
@IBOutlet private weak var headerTitleLabel: UILabel!
@IBOutlet private weak var loadingActivityIndicator: UIActivityIndicatorView!
// MARK: - Private
private func updateUI() {
private func setupUI() {
contentView.addSubview(headerTitleLabel)
contentView.addSubview(loadingActivityIndicator)
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
}
// MARK: - Public
func update(isLoading: Bool, title: String?) {
headerTitleLabel.text = title
if isLoading && title != nil {
@@ -10,38 +10,29 @@ import UIKit
import StoreKit
import Shogun
@objc
class SubscriptionsViewController: UITableViewController {
private static let SegueIdentifierSubscriptionDetail = "ShowSubscriptionDetail"
private static let CellHeightDescription: CGFloat = 320.0
// sezioni
private enum TableSection: CaseIterable {
case active
case description
case monthly
case yearly
case perpetual
case products
var sectionTitle: String? {
switch self {
case .monthly: return NSLocalizedString("inapp_monthly_subscriptions", comment: "")
case .yearly: return NSLocalizedString("inapp_yearly_subscriptions", comment: "")
case .perpetual: return NSLocalizedString("inapp_lifetime_subscriptions", comment: "")
default: return nil
case .products: NSLocalizedString("subscriptions_available", comment: "")
default: nil
}
}
}
private let sections = TableSection.allCases
private var allProducts = [SKProduct]()
private var monthlyProducts = [SKProduct]()
private var yearlyProducts = [SKProduct]()
private var perpetualProducts = [SKProduct]()
/// All products retrieved from AppStore
private var products = [EQNInAppProducts]()
/// Product already bought by the user
private var subscribedProduct: SKProduct?
private var productSubscribed: EQNInAppProducts?
/// Availability for subscriptions
private var availability: EQNPurchaseAvailability?
/// Tells if products are loading
@@ -49,6 +40,13 @@ class SubscriptionsViewController: UITableViewController {
/// Tells if a restore is in progress
private var isRestorePurchase = false
// MARK: - Init
@objc
convenience init() {
self.init(style: .plain)
}
// MARK: - View Lifecycle
override func viewDidLoad() {
@@ -64,15 +62,7 @@ class SubscriptionsViewController: UITableViewController {
loadData()
checkAvailabilities()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == Self.SegueIdentifierSubscriptionDetail,
let controller = segue.destination as? SubscriptionDetailViewController,
let product = sender as? SKProduct {
controller.product = product
}
}
// MARK: - Private
private func addObservers() {
@@ -90,75 +80,57 @@ class SubscriptionsViewController: UITableViewController {
}
private func configureUI() {
let restoreButton = UIBarButtonItem(title: NSLocalizedString("purchase_pro_restore", comment: ""),
style: .plain,
target: self,
action: #selector(restoreTapped(_:)))
navigationItem.rightBarButtonItem = restoreButton
// if is presented in Simulator, add done button
if navigationController?.viewControllers.first == self {
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(closeTapped(_:)))
navigationItem.leftBarButtonItem = doneButton
}
navigationItem.largeTitleDisplayMode = .never
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = Self.CellHeightDescription;
}
private func updateUI() {
monthlyProducts.removeAll()
yearlyProducts.removeAll()
perpetualProducts.removeAll()
// creates list to show
let isDiscountAvailable = checkDiscountPrice()
allProducts.forEach { (product) in
if isDiscountAvailable {
if product.productIdentifier == VersioneProProducts.Identifier.Subscription10kMonthly ||
product.productIdentifier == VersioneProProducts.Identifier.Subscription100kMonthly {
monthlyProducts.append(product)
} else if product.productIdentifier == VersioneProProducts.Identifier.Subscription10kYearlyDiscounted ||
product.productIdentifier == VersioneProProducts.Identifier.Subscription100kYearlyDiscounted {
yearlyProducts.append(product)
}
} else {
if product.productIdentifier == VersioneProProducts.Identifier.Subscription10kMonthly ||
product.productIdentifier == VersioneProProducts.Identifier.Subscription100kMonthly {
monthlyProducts.append(product)
}
else if product.productIdentifier == VersioneProProducts.Identifier.Subscription10kYearly ||
product.productIdentifier == VersioneProProducts.Identifier.Subscription100kYearly {
yearlyProducts.append(product)
}
}
// perpetual scribuscriptions doesn't have discounted version
if product.productIdentifier == VersioneProProducts.Identifier.Subscription10kPerpetual ||
product.productIdentifier == VersioneProProducts.Identifier.Subscription100kPerpetual {
perpetualProducts.append(product)
}
tableView.estimatedRowHeight = 600.0
tableView.separatorStyle = .none
tableView.backgroundColor = .systemGroupedBackground
tableView.contentInset = EQNBaseContainerTableViewCell.EdgeInsets
if #available(iOS 15.0, *) {
// remove extra padding on top of each section header
tableView.sectionHeaderTopPadding = 0.0
}
tableView.reloadData()
tableView.registerCell(for: SubscriptionsActiveTableViewCell.self)
tableView.registerCell(for: SubscriptionsDescriptionTableViewCell.self)
tableView.registerCell(for: SubscriptionProductTableViewCell.self)
tableView.registerHeaderFooterView(for: SubscriptionsHeaderTableViewCell.self)
}
private func loadData() {
isLoading = true
VersioneProProducts.store.requestProducts{ [weak self] success, products in
EQNInAppProducts.store.requestProducts { [weak self] success, storeProducts in
self?.isLoading = false
guard let self = self, let products = products, success == true else { return }
guard let self = self, let storeProducts, success == true else { return }
let purchased = products.filter { (product) -> Bool in
let isPurchased = VersioneProProducts.store.isProductPurchased(product.productIdentifier)
let isSubscription = VersioneProProducts.isSubscription(for: product.productIdentifier)
return isPurchased && isSubscription
}
self.subscribedProduct = purchased.first
self.allProducts = products.sorted(by: { $0.productIdentifier > $1.productIdentifier })
let products = storeProducts.compactMap { EQNInAppProducts.from(product: $0) }
self.updateUI()
let purchased = products
.filter { (product) -> Bool in
// filter for subscriptions
let isPurchased = EQNInAppProducts.store.isProductPurchased(product.productIdentifier)
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
}
return rIs10k
}
self.productSubscribed = purchased.first
self.products = products.sorted(by: { $0.productIdentifier > $1.productIdentifier })
self.tableView.reloadData()
}
}
@@ -171,18 +143,13 @@ class SubscriptionsViewController: UITableViewController {
EQNPurchaseUtility.availableSubscriptions { (availability) in
DispatchQueue.main.async {
self.availability = availability
self.updateUI()
self.tableView.reloadData()
}
}
}
// MARK: - Actions
@objc func restoreTapped(_ sender: AnyObject) {
isRestorePurchase = true
VersioneProProducts.store.restorePurchases()
}
@objc func closeTapped(_ sender: AnyObject) {
dismiss(animated: true, completion: nil)
}
@@ -190,7 +157,7 @@ class SubscriptionsViewController: UITableViewController {
// MARK: - Notifications
@objc func fail(_ notification: Notification){
VersioneProProducts.store.loadPurchase()
EQNInAppProducts.store.loadPurchase()
}
@objc func handlePurchaseNotification(_ notification: Notification) {
@@ -209,7 +176,7 @@ class SubscriptionsViewController: UITableViewController {
present(alert, animated: true, completion: nil)
}
VersioneProProducts.store.loadPurchase()
EQNInAppProducts.store.loadPurchase()
loadData()
}
@@ -228,20 +195,22 @@ class SubscriptionsViewController: UITableViewController {
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let tableSection = sections[section]
if let cell = tableView.dequeueReusableCell(withIdentifier: "SectionHeaderCell") as? SubscriptionsHeaderTableViewCell {
cell.title = tableSection.sectionTitle
cell.isLoading = isLoading
return cell
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
}
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 {
@@ -249,96 +218,59 @@ class SubscriptionsViewController: UITableViewController {
switch tableSection {
case .active: return 1
case .description: return 1
case .monthly,
.yearly,
.perpetual:
return availableProducts(for: tableSection).count
case .products: return products.isEmpty ? 0 : 2
}
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let tableSection = sections[indexPath.section]
if tableSection == .description {
// autolayout in description doesn't work 🤷
return Self.CellHeightDescription
}
return UITableView.automaticDimension
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let tableSection = sections[indexPath.section]
if tableSection == .active || tableSection == .description {
return
}
// add round borders to first and last row in products cells
let cornerRadius = AppTheme.shared.cardCornerRadius
var corners: UIRectCorner = []
if indexPath.row == 0 {
corners.update(with: .topLeft)
corners.update(with: .topRight)
}
if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 {
corners.update(with: .bottomLeft)
corners.update(with: .bottomRight)
}
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: cell.bounds,
byRoundingCorners: corners,
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)).cgPath
cell.layer.mask = maskLayer
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableSection = sections[indexPath.section]
if tableSection == .active {
let cell = tableView.dequeueReusableCell(withIdentifier: "ActiveSubscriptionsCell", for: indexPath) as! SubscriptionsActiveTableViewCell
cell.product = subscribedProduct
return cell
}
if tableSection == .description {
let cell = tableView.dequeueReusableCell(withIdentifier: "DescriptionCell", for: indexPath) as! SubscriptionsDescriptionTableViewCell
return cell
}
let products = availableProducts(for: tableSection)
let cell = tableView.dequeueReusableCell(withIdentifier: "SubscriptionCell", for: indexPath) as! SubscriptionProductTableViewCell
cell.product = products[indexPath.row]
cell.availability = availability
return cell
switch tableSection {
case .active:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SubscriptionsActiveTableViewCell.self, for: indexPath)
cell.selectionStyle = .none
cell.update(with: productSubscribed)
cell.onTapRestore = { [weak self] in
guard let self else { return }
self.isRestorePurchase = true
EQNInAppProducts.store.restorePurchases()
}
return cell
case .description:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SubscriptionsDescriptionTableViewCell.self, for: indexPath)
cell.selectionStyle = .none
return cell
case .products:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SubscriptionProductTableViewCell.self, for: indexPath)
let category: EQNInAppProducts.Category = switch indexPath.row {
case 0: .top10k
case 1: .top100k
default: .top100k
}
cell.update(category: category, availability: availability)
return cell
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let tableSection = sections[indexPath.section]
let products = availableProducts(for: tableSection)
let products = availableProducts(for: indexPath)
if !products.isEmpty {
performSegue(withIdentifier: Self.SegueIdentifierSubscriptionDetail, sender: products[indexPath.row])
let controller = SubscriptionDetailsViewController(products: products)
navigationController?.pushViewController(controller, animated: true)
}
}
// MARK: - Helpers
private func availableProducts(for section: TableSection) -> [SKProduct] {
switch section {
case .monthly: return monthlyProducts
case .yearly: return yearlyProducts
case .perpetual: return perpetualProducts
private func availableProducts(for indexPath: IndexPath) -> [EQNInAppProducts] {
let section = sections[indexPath.section]
switch (section, indexPath.row) {
case (.products, 0): return products.filter { $0.isTop10k }
case (.products, 1): return products.filter { $0.isTop100k }
default: return []
}
}
}
extension SubscriptionsViewController: StoryboardInitializable {
static var storyboardName: String {
"Main"
}
static var storyboardControllerId: String {
"subscriptionsController"
}
}
@@ -15,6 +15,8 @@ class RealtimeAlertContainerView: UIView {
lazy var alertView: RealtimeAlertView = {
let view = RealtimeAlertView()
view.translatesAutoresizingMaskIntoConstraints = false
view.eqn_applyRoundedCorners()
view.clipsToBounds = true
return view
}()
@@ -78,10 +80,11 @@ class RealtimeAlertView: UIView {
let waveTimeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .title2)
label.font = .preferredFont(forTextStyle: .largeTitle)
label.textColor = AppTheme.Colors.red
label.text = String.localizedStringWithFormat(NSLocalizedString("alert_wave", comment: ""), 0)
label.textAlignment = .center
label.numberOfLines = 2
label.isHidden = true
return label
}()
@@ -89,7 +92,7 @@ class RealtimeAlertView: UIView {
let intensityLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .title3)
label.font = .preferredFont(forTextStyle: .largeTitle)
label.textColor = AppTheme.Colors.red
label.textAlignment = .center
label.numberOfLines = 2
@@ -9,46 +9,97 @@
import UIKit
import Shogun
class SegnalazioniLast24HoursCell: EQNBaseTableViewCell {
@objc
class SegnalazioniLast24HoursCell: EQNBaseContainerTableViewCell {
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var reportsLabel: UILabel!
@IBOutlet private weak var reportsDescriptionLabel: UILabel!
@objc var onTapTwitter: (() -> Void)?
@objc var onTapMap: (() -> Void)?
@objc var onTapTelegram: (() -> Void)?
override var isHeaderVisible: Bool { false }
// MARK: - UI
private lazy var reportsLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.Colors.red
label.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
private lazy var reportsDescriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
@IBOutlet private weak var twitterButton: UIButton! {
didSet {
twitterButton.imageView?.contentMode = .scaleAspectFit
}
}
@IBOutlet private weak var telegramButton: UIButton! {
didSet {
telegramButton.imageView?.contentMode = .scaleAspectFit
}
}
@IBOutlet private weak var mapButton: UIButton!
private lazy var twitterButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(twitterButtonTapped(_:)))
button.imageView?.contentMode = .scaleAspectFit
button.setImage(.init(named: "xcorp_icon"), for: .normal)
return button
}()
private lazy var mapButton: UIButton = {
let button = EQNRoundedButton.make(title: NSLocalizedString("official_button_map", comment: ""), target: self, action: #selector(mapButtonTapped(_:)))
return button
}()
private lazy var telegramButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(telegramButtonTapped(_:)))
button.imageView?.contentMode = .scaleAspectFit
button.setImage(.init(named: "telegram_icon"), for: .normal)
return button
}()
// MARK: - View Lifecycle
// MARK: - Internal
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(reportsLabel)
containerView.addSubview(reportsDescriptionLabel)
let stackView = UIStackView(arrangedSubviews: [twitterButton, mapButton, telegramButton])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.spacing = .cardVerticalSpacing
stackView.distribution = .fillEqually
containerView.addSubview(stackView)
reportsLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardPadding).isActive = true
reportsLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
reportsLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
reportsDescriptionLabel.topAnchor.constraint(equalTo: reportsLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
reportsDescriptionLabel.leadingAnchor.constraint(equalTo: reportsLabel.leadingAnchor).isActive = true
reportsDescriptionLabel.trailingAnchor.constraint(equalTo: reportsLabel.trailingAnchor).isActive = true
stackView.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
stackView.topAnchor.constraint(equalTo: reportsDescriptionLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
stackView.leadingAnchor.constraint(equalTo: reportsDescriptionLabel.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: reportsDescriptionLabel.trailingAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("tab_manual", comment: "").capitalized
override func updateUI() {
super.updateUI()
reportsDescriptionLabel.text = NSLocalizedString("main_map", comment: "")
mapButton.setTitle(NSLocalizedString("official_button_map", comment: ""), for: .normal)
}
// MARK: - Public
@objc func updateUI(for smartphoneNetwork: EQNReteSmartphone?) {
guard let smartphoneNetwork = smartphoneNetwork else {
return
}
@objc
func update(with smartphoneNetwork: EQNReteSmartphone?) {
guard let smartphoneNetwork = smartphoneNetwork else { return }
let reports = smartphoneNetwork.manual
self.reportsLabel.text = "\(reports)"
@@ -61,4 +112,18 @@ class SegnalazioniLast24HoursCell: EQNBaseTableViewCell {
self.reportsLabel.textColor = UIColor(hex6: 0xff0000)
}
}
// MARK: - Actions
@objc private func twitterButtonTapped(_ sender: UIButton) {
onTapTwitter?()
}
@objc private func mapButtonTapped(_ sender: UIButton) {
onTapMap?()
}
@objc private func telegramButtonTapped(_ sender: UIButton) {
onTapTelegram?()
}
}
@@ -28,35 +28,7 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
private var filteredReports = [EQNSegnalazione]()
// MARK: - UI
// app icon and name displayed on the screenshot
private lazy var watermarkView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = true
let logo = UIImageView(image: .init(named: "eq_icon_transparent"))
logo.translatesAutoresizingMaskIntoConstraints = false
logo.contentMode = .scaleAspectFit
view.addSubview(logo)
logo.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
logo.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
logo.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
logo.widthAnchor.constraint(equalTo: logo.heightAnchor).isActive = true
let title = UILabel()
title.translatesAutoresizingMaskIntoConstraints = false
title.text = NSLocalizedString("app_name", comment: "") + " App"
title.textColor = AppTheme.Colors.red
title.font = .preferredFont(forTextStyle: .title3, weight: .semibold)
view.addSubview(title)
title.leadingAnchor.constraint(equalTo: logo.trailingAnchor, constant: 10.0).isActive = true
title.centerYAnchor.constraint(equalTo: logo.centerYAnchor).isActive = true
title.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
return view
}()
private lazy var magnitudeLegendView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
@@ -95,16 +67,11 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
override func extraUI() {
view.addSubview(magnitudeLegendView)
view.addSubview(watermarkView)
magnitudeLegendView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
magnitudeLegendView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
magnitudeLegendView.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
magnitudeLegendView.topAnchor.constraint(equalTo: mapView.topAnchor).isActive = true
watermarkView.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 10.0).isActive = true
watermarkView.leadingAnchor.constraint(equalTo: mapView.leadingAnchor, constant: 10.0).isActive = true
watermarkView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
}
override func configureUI() {
@@ -153,7 +120,7 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
.first
// controlliamo che sia inferiore al raggio impostato per le notifiche
if let radius = Double(EQNNotificheSegnalazioniUtente.shared().distanzaPosizione),
if let radius = Double(EQNSettingUserReportNotification.shared.distanzaMassima),
let nearestCluser = nearestCluser,
abs(nearestCluser.distance(from: userPosition)) < radius {
centerLocation = nearestCluser
@@ -191,33 +158,20 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
@objc private func onTapMapDetailStyleButton(_ sender: Any) {
appPreferences.userReportExpandedView.toggle()
loadDataSource()
reloadMap()
}
@objc private func onTapScreenshotButton(_ sender: Any) {
let snapshot = createSnapshot()
let controller = UIActivityViewController(activityItems: [snapshot], applicationActivities: [])
present(controller, animated: true)
}
public func createSnapshot() -> UIImage {
// mostriamo il watermark e nascondiamo la legenda
watermarkView.isHidden = false
magnitudeLegendView.isHidden = true
// riduciamo la porzione da salvare alla sola mappa (eliminiamo i filtri)
let size = CGSize(width: view.bounds.width, height: mapView.bounds.maxY)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { ctx in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
let screenshot = createSnapshot {
// nascondiamo la legenda
magnitudeLegendView.isHidden = true
} restore: {
// ri-visualizziamo la legenda
magnitudeLegendView.isHidden = false
}
// torniamo allo stato originale
watermarkView.isHidden = true
magnitudeLegendView.isHidden = false
return image
let controller = UIActivityViewController(activityItems: [screenshot], applicationActivities: [])
present(controller, animated: true)
}
// MARK: - Private
@@ -233,10 +187,10 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
var cluster_code = 0
var vector_cluster = [Int](repeating: 0, count: vector_latitude.count)
for i in 0..<vector_latitude.count {
let deltaMinute_i = getDeltaMinute(vector_date[i])
let deltaMinute_i = EQNUtility.getDeltaMinute(vector_date[i])
if vector_cluster[i] == 0 && deltaMinute_i <= minutes {
for j in 0..<vector_latitude.count {
let deltaMinute_j = getDeltaMinute(vector_date[j])
let deltaMinute_j = EQNUtility.getDeltaMinute(vector_date[j])
if i != j && deltaMinute_j <= minutes {
if abs(vector_latitude[i] - vector_latitude[j]) < 4 && abs(vector_longitude[i] - vector_longitude[j]) < 4 && abs(deltaMinute_i - deltaMinute_j) <= 20 {
if vector_cluster[j] > 0 {
@@ -327,10 +281,6 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
mapView.addOverlays(overlays)
}
private func getDeltaMinute(_ date: Date) -> TimeInterval {
Date().timeIntervalSince(date) / 60.0
}
// MARK: - Map
override func setupAnnotationView(for annotation: MKAnnotation, on mapView: MKMapView) -> MKAnnotationView? {
@@ -7,49 +7,100 @@
//
import UIKit
import Shogun
class SegnalazioniSendReportCell: EQNBaseTableViewCell {
class SegnalazioniSendReportCell: EQNBaseContainerTableViewCell {
private struct Report: Equatable {
let magnitude: Int
let text: String
let color: UIColor
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.magnitude == rhs.magnitude
}
}
@objc var onTapReport: (_ magnitude: Int) -> Void = { _ in }
override var headerText: String { NSLocalizedString("main_feel", comment: "") }
// MARK: - UI
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var reportMercalli2: UILabel!
@IBOutlet private weak var reportMercalli3: UILabel!
@IBOutlet private weak var reportMercalli4: UILabel!
@IBOutlet private weak var reportMercalli5: UILabel!
@IBOutlet private weak var reportMercalli6: UILabel!
@IBOutlet private weak var reportMercalli7: UILabel!
@IBOutlet private weak var reportMercalli8: UILabel!
@IBOutlet private weak var reportMercalli9: UILabel!
@IBOutlet private weak var reportMercalli10: UILabel!
@IBOutlet private weak var reportMercalli11: UILabel!
@IBOutlet private weak var reportMercalli12: UILabel!
// MARK: - View Lifecycle
private let reports: [Report] = [
.init(magnitude: 20, text: NSLocalizedString("mercalli_II", comment: ""), color: .init(named: "Mercalli 20")!),
.init(magnitude: 30, text: NSLocalizedString("mercalli_III", comment: ""), color: .init(named: "Mercalli 30")!),
.init(magnitude: 40, text: NSLocalizedString("mercalli_IV", comment: ""), color: .init(named: "Mercalli 40")!),
.init(magnitude: 50, text: NSLocalizedString("mercalli_V", comment: ""), color: .init(named: "Mercalli 50")!),
.init(magnitude: 60, text: NSLocalizedString("mercalli_VI", comment: ""), color: .init(named: "Mercalli 60")!),
.init(magnitude: 70, text: NSLocalizedString("mercalli_VII", comment: ""), color: .init(named: "Mercalli 70")!),
.init(magnitude: 80, text: NSLocalizedString("mercalli_VIII", comment: ""), color: .init(named: "Mercalli 80")!),
.init(magnitude: 90, text: NSLocalizedString("mercalli_IX", comment: ""), color: .init(named: "Mercalli 90")!),
.init(magnitude: 100, text: NSLocalizedString("mercalli_X", comment: ""), color: .init(named: "Mercalli 100")!),
.init(magnitude: 110, text: NSLocalizedString("mercalli_XI", comment: ""), color: .init(named: "Mercalli 110")!),
.init(magnitude: 120, text: NSLocalizedString("mercalli_XII", comment: ""), color: .init(named: "Mercalli 120")!)
]
override func awakeFromNib() {
super.awakeFromNib()
// MARK: - Internal
override func setupUI() {
super.setupUI()
localizeUI()
var previousView = topView
reports.enumerated().forEach { index, report in
let view = createContentView(magnitude: report.magnitude, text: report.text, color: report.color)
containerView.addSubview(view)
view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
let padding: CGFloat = report == reports.first ? .cardPadding : 0
view.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: padding).isActive = true
if report == reports.last {
view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
}
previousView = view
}
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("main_feel", comment: "")
reportMercalli2.text = NSLocalizedString("mercalli_II", comment: "")
reportMercalli3.text = NSLocalizedString("mercalli_III", comment: "")
reportMercalli4.text = NSLocalizedString("mercalli_IV", comment: "")
reportMercalli5.text = NSLocalizedString("mercalli_V", comment: "")
reportMercalli6.text = NSLocalizedString("mercalli_VI", comment: "")
reportMercalli7.text = NSLocalizedString("mercalli_VII", comment: "")
reportMercalli8.text = NSLocalizedString("mercalli_VIII", comment: "")
reportMercalli9.text = NSLocalizedString("mercalli_IX", comment: "")
reportMercalli10.text = NSLocalizedString("mercalli_X", comment: "")
reportMercalli11.text = NSLocalizedString("mercalli_XI", comment: "")
reportMercalli12.text = NSLocalizedString("mercalli_XII", comment: "")
private func createContentView(
magnitude: Int,
text: String,
color: UIColor
) -> UIView {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = color
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .body)
label.textColor = AppTheme.shared.cardTextColor
label.numberOfLines = 0
label.text = text
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .clear
button.tag = magnitude
button.addTarget(self, action: #selector(onTapMagnitudeButton(_:)), for: .touchUpInside)
view.addSubview(label)
view.addSubview(button)
// use a custom vertical spacing to make single lines bigger
let verticalSpacing: CGFloat = 15.0
label.topAnchor.constraint(equalTo: view.topAnchor, constant: verticalSpacing).isActive = true
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: .cardPadding).isActive = true
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: .cardPadding.negative).isActive = true
label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -verticalSpacing).isActive = true
button.constraint(to: view)
return view
}
// MARK: - Actions
@@ -58,4 +109,9 @@ class SegnalazioniSendReportCell: EQNBaseTableViewCell {
let magnitude = sender.tag
onTapReport(magnitude)
}
@objc private func onTapMagnitudeButton(_ sender: UIButton) {
let magnitude = sender.tag
onTapReport(magnitude)
}
}
@@ -38,8 +38,12 @@
- (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"];
}
- (void)refreshUI
@@ -74,8 +78,17 @@
{
if (indexPath.row == 0) {
SegnalazioniLast24HoursCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Last24HCell" forIndexPath:indexPath];
EQNReteSmartphone *reteSmartPhone = [EQNManager defaultManager].rete_smartphone;
[cell updateUIFor:reteSmartPhone];
[cell updateWith:[EQNManager defaultManager].rete_smartphone];
__weak SegnalazioniViewController *weakSelf = self;
cell.onTapMap = ^{
[weakSelf openMap];
};
cell.onTapTwitter = ^{
[weakSelf openTwitter];
};
cell.onTapTelegram = ^{
[weakSelf openTelegram];
};
return cell;
}
@@ -88,21 +101,21 @@
#pragma mark - Actions
- (IBAction)openMapTapped:(id)sender
- (void)openMap
{
SegnalazioniMapViewController *controller = [[SegnalazioniMapViewController alloc] init];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:controller];
[self presentViewController:navController animated:YES completion:nil];
}
- (IBAction)openTwitterTapped:(id)sender
- (void)openTwitter
{
NSURL *twitterUrl = [NSURL URLWithString:EQNTwitterProfileUrl];
SFSafariViewController *controller = [[SFSafariViewController alloc] initWithURL:twitterUrl];
[self presentViewController:controller animated:YES completion:nil];
}
- (IBAction)openTelegramTapped:(id)sender
- (void)openTelegram
{
NSURL *telegramUrl = [NSURL URLWithString:EQNTelegramUrl];
[[UIApplication sharedApplication] openURL:telegramUrl options:@{} completionHandler:nil];
@@ -28,7 +28,7 @@ class SeismicNetworkAdvertiseTableViewCell: UITableViewCell {
return view
}()
private lazy var bannerView: GADNativeAdView = {
private lazy var bannerView: NativeAdView = {
let view = GADTMediumTemplateView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
@@ -71,7 +71,7 @@ class SeismicNetworkAdvertiseTableViewCell: UITableViewCell {
// MARK: - Public
func loadNativeAd(_ nativeAd: GADNativeAd) {
func loadNativeAd(_ nativeAd: NativeAd) {
bannerView.nativeAd = nativeAd
}
}
@@ -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,232 @@
//
// 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
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 seismicNetworkCellDidTapCalendar(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapSettings(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkTableViewCell)
}
class SeismicNetworkTableViewCell: UITableViewCell {
static let Identifier = "SeismicNetworkTableViewCell"
typealias MagnitudeColors = (textColor: UIColor, startColor: UIColor, endColor: UIColor)
/// 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 buttons
}
class SeismicNetworkTableViewCell: SeismicNetworkBaseTableViewCell {
/// Available cell type
enum DisplayType {
@@ -45,45 +21,15 @@ class SeismicNetworkTableViewCell: UITableViewCell {
/// Cell with map visible
case mapExpanded
}
/// Delegate
weak var delegate: SeismicNetworkTableViewCellDelegate?
// MARK: - Internal
private static let DefaultVerticalSpacing: CGFloat = 6.0
private static let DefaultBodyFont = UIFont.preferredFont(forTextStyle: .body)
private static let DefaultBodyFontLight = UIFont.preferredFont(forTextStyle: .body, weight: .light)
/// Seismic to show
private var seismic: EQNSisma?
private(set) var displayType = DisplayType.normal
private var informationTypes = [InformationType]()
private var colors: MagnitudeColors?
private var isPushSelected = false
// MARK: - UI Components
private lazy var containerView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = AppTheme.shared.cardCornerRadius
view.layer.masksToBounds = false
// add shadow
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.5
view.layer.shadowOffset = CGSize(width: 0, height: 2)
view.layer.shadowRadius = 2
return view
}()
private lazy var titleImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var placeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
@@ -92,11 +38,20 @@ 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
label.backgroundColor = UIColor.white.withAlphaComponent(0.5)
label.textAlignment = .center
label.textAlignment = .right
label.font = .preferredFont(forTextStyle: .subheadline)
label.numberOfLines = 2
return label
}()
@@ -111,35 +66,40 @@ class SeismicNetworkTableViewCell: UITableViewCell {
private lazy var depthLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Self.DefaultBodyFontLight
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var timeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Self.DefaultBodyFontLight
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var distanceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Self.DefaultBodyFontLight
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var coordinateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Self.DefaultBodyFontLight
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var populationLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Self.DefaultBodyFontLight
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
@@ -147,8 +107,9 @@ class SeismicNetworkTableViewCell: UITableViewCell {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = Self.DefaultBodyFont
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
@@ -156,8 +117,9 @@ class SeismicNetworkTableViewCell: UITableViewCell {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = Self.DefaultBodyFont
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
@@ -183,18 +145,19 @@ class SeismicNetworkTableViewCell: UITableViewCell {
setupUI()
}
// MARK: - View Lifecycle
override func layoutSubviews() {
super.layoutSubviews()
containerView.eqn_applyShadowAndRoundedCorners()
gradientView.eqn_applyRoundedCorners()
}
// 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()
// this variable is used to keep track of the previous view, in order to attach proper constraints
var previousView: UIView = containerView
@@ -216,48 +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(titleImageView)
stackViewTitle.addArrangedSubview(placeLabel)
stackViewTitle.addArrangedSubview(networkLabel)
stackViewTitle.addArrangedSubview(shareButton)
titleImageView.heightAnchor.constraint(equalToConstant: titleComponentsHeight).isActive = true
titleImageView.widthAnchor.constraint(equalTo: titleImageView.heightAnchor).isActive = true
networkLabel.heightAnchor.constraint(equalToConstant: 34.0).isActive = true
networkLabel.setContentHuggingPriority(.init(800), for: .horizontal)
networkLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
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) {
@@ -287,20 +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
if informationTypes.contains(.realtimeSmartphones) || informationTypes.contains(.reportUsers) {
let separator2 = addSeparator(constraintTo: stackViewInformations.bottomAnchor)
// network
containerView.addSubview(networkLabel)
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, 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)
@@ -308,34 +257,43 @@ class SeismicNetworkTableViewCell: UITableViewCell {
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: separator2.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
stackViewReports.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor, constant: 20.0).isActive = true
stackViewReports.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor, constant: -20.0).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
stackViewButtons.axis = .horizontal
stackViewButtons.distribution = .fillEqually
stackViewButtons.spacing = 4
stackViewButtons.spacing = 8
let buttonMap = createRoundedButton(title: "🗺", action: #selector(mapTapped(_:)))
let buttonMap = EQNRoundedButton.make(title: "🗺", target: self, action: #selector(mapTapped(_:)))
stackViewButtons.addArrangedSubview(buttonMap)
let buttonCalendar = createRoundedButton(title: "📆", action: #selector(calendarTapped(_:)))
let buttonCalendar = EQNRoundedButton.make(title: "📆", target: self, action: #selector(calendarTapped(_:)))
stackViewButtons.addArrangedSubview(buttonCalendar)
let buttonSettings = createRoundedButton(title: "🔧", action: #selector(settingsTapped(_:)))
let buttonSettings = EQNRoundedButton.make(title: "🔧", target: self, action: #selector(settingsTapped(_:)))
stackViewButtons.addArrangedSubview(buttonSettings)
containerView.addSubview(stackViewButtons)
stackViewButtons.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
stackViewButtons.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
stackViewButtons.heightAnchor.constraint(equalToConstant: Self.DefaultButtonHeight).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
@@ -345,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
@@ -353,10 +311,11 @@ class SeismicNetworkTableViewCell: UITableViewCell {
}
if (displayType == .mapExpanded) {
let buttonClose = createRoundedButton(title: NSLocalizedString("official_close", comment: "").uppercased(), action: #selector(closeTapped(_:)))
let buttonClose = EQNRoundedButton.make(title: NSLocalizedString("official_close", comment: "").uppercased(), target: self, action: #selector(closeTapped(_:)))
containerView.addSubview(buttonClose)
buttonClose.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
buttonClose.heightAnchor.constraint(equalToConstant: Self.DefaultButtonHeight).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
@@ -364,12 +323,9 @@ class SeismicNetworkTableViewCell: UITableViewCell {
else {
previousView.bottomAnchor.constraint(equalTo: containerView.layoutMarginsGuide.bottomAnchor).isActive = true
}
}
private func recreateUI() {
// remove all subviews and recreate the required components
containerView.subviews.forEach({ $0.removeFromSuperview() })
setupUI()
containerView.eqn_applyShadowAndRoundedCorners()
gradientView.eqn_applyRoundedCorners()
}
private func updateUI() {
@@ -377,15 +333,13 @@ class SeismicNetworkTableViewCell: UITableViewCell {
let viewModel = SeismicNetworkViewModel(seismic: seismic)
containerView.backgroundColor = colors?.startColor
let notified = couldBeNotified(for: seismic)
titleImageView.image = notified ? UIImage(named: "bell") : UIImage(named: "bell_disabled")
gradientView.image = .gradient(from: viewModel.colors.startColor, to: viewModel.colors.endColor, with: .init(origin: .zero, size: .init(width: 500, height: 1)))
// update seismic data
placeLabel.text = viewModel.place
networkLabel.text = viewModel.network + " " // add some padding
magnitudeLabel.textColor = colors?.textColor
placeLabel.textColor = isPushSelected ? AppTheme.Colors.pureBlue : AppTheme.shared.cardTextColor
networkLabel.text = String(format: NSLocalizedString("official_provider", comment: ""), viewModel.network)
magnitudeLabel.textColor = viewModel.colors.textColor
magnitudeLabel.text = viewModel.magnitude
depthLabel.text = viewModel.depth
timeLabel.text = "🕗 \(viewModel.time)"
@@ -404,7 +358,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
alertsLabel.text = "⚠️ \(viewModel.users)"
}
if displayType == .mapExpanded {
// zoom based on population involved
let longitudeSpan = mapSpanLongitude(population: seismic.population100km)
@@ -431,11 +384,16 @@ class SeismicNetworkTableViewCell: UITableViewCell {
/// - seismic: Seismic to display
/// - type: Type of cell
/// - informations: Informations to show
public func configure(with seismic: EQNSisma, type: DisplayType, informations: [InformationType]) {
public func configure(
with seismic: EQNSisma,
type: DisplayType,
informations: [InformationType],
isPushSelected: Bool
) {
self.seismic = seismic
self.colors = calculateColors(for: seismic.magnitude.doubleValue)
self.displayType = type
self.informationTypes = informations
self.isPushSelected = isPushSelected
if !informations.contains(.time) {
self.informationTypes += [.time]
@@ -450,6 +408,9 @@ class SeismicNetworkTableViewCell: UITableViewCell {
if seismic.userNumber.intValue > 0 && !informations.contains(.reportUsers) {
self.informationTypes += [.reportUsers]
}
if seismic.isoCode == "0" && informations.contains(.intensityMap) {
self.informationTypes.removeAll { $0 == .intensityMap }
}
recreateUI()
updateUI()
@@ -457,90 +418,38 @@ 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 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
}
private func createRoundedButton(title: String, action: Selector) -> EQNRoundedButton {
let button = EQNRoundedButton(frame: .zero)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: action, for: .touchUpInside)
button.setTitle(title, for: .normal)
button.setTitleColor(AppTheme.Colors.darkGray, for: .normal)
button.backgroundColor = UIColor.white.withAlphaComponent(0.5)
return button
}
/// Check if the user could be received a notification for this seismic
private func couldBeNotified(for seismic: EQNSisma) -> Bool {
let settings = EQNNotificheReteSismiche.shared()
if !settings.isAbilitato {
return false
}
if !settings.listaEnti.contains(seismic.provider) {
return false
}
var notified = true
if let radius = Double(settings.distanzaPosizione), seismic.userDistance > radius {
notified = false
}
if let magnitude = Double(settings.energiaSisma), seismic.magnitude.doubleValue < magnitude {
notified = false
}
if settings.isAbilitaVicini, seismic.userDistance < 50 {
notified = true
}
if settings.isTerremortiForti, let strongMagnitude = Double(settings.energiaTerremotiForti), seismic.magnitude.doubleValue >= strongMagnitude {
notified = true
}
return notified
}
/// Determines the zoom for the map, based on the involved population
private func mapSpanLongitude(population: Double) -> CLLocationDegrees {
var zoom: CLLocationDegrees = 1
@@ -553,55 +462,4 @@ class SeismicNetworkTableViewCell: UITableViewCell {
}
return zoom
}
/// 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 (textColor: textColor, startColor: startColor, endColor: endColor)
}
}
@@ -1,48 +0,0 @@
//
// FiltersViewModel.swift
// Earthquake Network
//
// Created by Andrea Busi on 22/03/21.
// Copyright © 2021 Earthquake Network. All rights reserved.
//
import Foundation
struct FiltersViewModel {
let magnitude: String
let distance: String
let timeframe: String
init() {
let magnitudoMinima = EQNData.magitudoDebole(for: EQNSeismic.shared.magnitudoMinima)
self.magnitude = Self.formattedMagnitude(magnitudoMinima.value)
let distanzaMassima = EQNData.raggioSisma(for: EQNSeismic.shared.distanzaMassima)
self.distance = Self.formattedDistance(distanzaMassima.value)
let periodoTemporale = EQNData.periodoTemporale(for: EQNSeismic.shared.periodoTemporale)
self.timeframe = Self.formattedTimeframe(periodoTemporale.value)
}
// MARK: - Private
private static func formattedMagnitude(_ magnitude: String) -> String {
return magnitude
}
private static func formattedDistance(_ distance: String) -> String {
if distance == EQNData.MaxRaggioSisma {
return ""
}
return "\(distance)km"
}
private static func formattedTimeframe(_ timeframe: String) -> String {
let time = Int(timeframe) ?? 0
if time < 60 {
return "\(time)m"
}
return "\(time/60)h"
}
}
@@ -16,13 +16,12 @@ protocol SeismicFiltersViewControllerDelegate: AnyObject {
class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private enum RowIdentifier: Int {
case magnitudoMinima
case sismiNelRaggio
case distanzaMassima
case periodoTemporale
case sismiFortiAbilita
case sismiFortiDistanza
case sismiQualsiasiMagnitudo
case modificaImpostazioni
case magnitudoMinima
case sismiRilevanti
case sismiTutti
case sismiPercepiti
}
weak var delegate: SeismicFiltersViewControllerDelegate?
@@ -37,29 +36,21 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
@IBOutlet private weak var closeButton: UIButton!
private var settings = [
SettingItem(type: .slider, title: NSLocalizedString("filter_magnitude", comment: "")),
SettingItem(type: .slider, title: NSLocalizedString("filter_distance", comment: "")),
SettingItem(type: .slider, title: NSLocalizedString("filter_timeframe", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_strong", comment: "")),
SettingItem(type: .slider, title: NSLocalizedString("options_strong_magnitude", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_near", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_reflect", comment: ""))
SettingItem(type: .enable, title: NSLocalizedString("filter_show_area", comment: "")),
SettingItem(type: .slider, title: ""),
SettingItem(type: .slider, title: NSLocalizedString("filter_minimum_magnitude", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_show_relevant", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_show_all", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_show_felt", comment: ""))
]
private let dataSourceMagnitudoMinima = EQNData.magitudoDeboli()
private let dataSourceDistanzaMassima = EQNData.raggioSismi()
private let dataSourcePeriodoTemporale = EQNData.periodiTemporali()
private let dataSourceSismiForti = EQNData.magitudoForti()
private var initialMagnitudoMinima: EQNGenericValue?
private var initialQualsiasiMagnitudo: Bool?
private let initialFilterType = EQNSeismic.shared.filterOption
private var currentFilterType = EQNSeismic.FilterType.inRadius
private var currentMaximumDistance = EQNData.DefaultFilterRadius
private var currentMinimumMagnitude = EQNData.DefaultFilterMagnitude
private var currentMagnitudoMinima = EQNData.DefaultMagitudoDebole
private var currentDistanzaMassima = EQNData.DefaultRaggioSisma
private var currentPeriodoTemporale = EQNData.DefaultPeriodoTemporale
private var currentSismiFortiAbilitati = false
private var currentSismiFortiDistanza = EQNData.DefaultMagitudoForte
private var currentSismiQualsiasiMagnitudo = false
private var currentModificaImpostazioni = false
private let dataSourceMaximumDistance = EQNData.filterRadius
private let dataSourceMinimumMagnitude = EQNData.filterMagnitude
// MARK: - View Lifecycle
@@ -86,19 +77,9 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
}
private func loadDataSource() {
currentMagnitudoMinima = EQNData.magitudoDebole(for: EQNSeismic.shared.magnitudoMinima)
if initialMagnitudoMinima == nil {
initialMagnitudoMinima = currentMagnitudoMinima
}
currentDistanzaMassima = EQNData.raggioSisma(for: EQNSeismic.shared.distanzaMassima)
currentPeriodoTemporale = EQNData.periodoTemporale(for: EQNSeismic.shared.periodoTemporale)
currentSismiFortiAbilitati = EQNSeismic.shared.sismiFortiAbilitati
currentSismiFortiDistanza = EQNData.magitudoForte(for: EQNSeismic.shared.sismiFortiMagnitudo)
currentSismiQualsiasiMagnitudo = EQNSeismic.shared.sismiQualsiasiAbilitati
if initialQualsiasiMagnitudo == nil {
initialQualsiasiMagnitudo = currentSismiQualsiasiMagnitudo
}
currentModificaImpostazioni = EQNSeismic.shared.modificaImpostazioniAbilitato
currentFilterType = EQNSeismic.shared.filterOption
currentMaximumDistance = EQNData.filterRadius(for: EQNSeismic.shared.maximumDistance)
currentMinimumMagnitude = EQNData.filterMagnitude(for: EQNSeismic.shared.minimumMagnitude)
}
// MARK: - Table view delegate and data source
@@ -108,45 +89,36 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let identifier = RowIdentifier(rawValue: indexPath.row) else {
return UITableViewCell()
}
let setting = settings[indexPath.row]
let isLocationAvailable = EQNUser.default().lastPosition != nil
switch setting.type {
case .slider:
let cell = SettingSliderTableViewCell(style: .default, reuseIdentifier: nil)
cell.titleLabel.text = setting.displayTitle
if indexPath.row == RowIdentifier.magnitudoMinima.rawValue {
cell.configureSlider(with: dataSourceMagnitudoMinima, current: currentMagnitudoMinima)
cell.valueChanged = { [unowned self] value in
currentMagnitudoMinima = value
EQNSeismic.shared.magnitudoMinima = value.value
EQNSeismic.shared.saveFilters()
let isFilterInRadiusEnabled = currentFilterType == .inRadius && isLocationAvailable
switch identifier {
case .distanzaMassima:
cell.isDisabled = !isFilterInRadiusEnabled
cell.isUserInteractionEnabled = isFilterInRadiusEnabled
cell.configureSlider(with: dataSourceMaximumDistance, current: currentMaximumDistance)
cell.valueChanged = { [weak self] value in
self?.onChangeMaximumDistance(value)
}
cell.dragEnded = { [unowned self] in
showWarningAlertIfNeeded(for: currentMagnitudoMinima)
}
} else if indexPath.row == RowIdentifier.distanzaMassima.rawValue {
cell.configureSlider(with: dataSourceDistanzaMassima, current: currentDistanzaMassima)
cell.valueChanged = { [unowned self] value in
currentDistanzaMassima = value
EQNSeismic.shared.distanzaMassima = value.value
EQNSeismic.shared.saveFilters()
}
} else if indexPath.row == RowIdentifier.periodoTemporale.rawValue {
cell.configureSlider(with: dataSourcePeriodoTemporale, current: currentPeriodoTemporale)
cell.valueChanged = { [unowned self] value in
currentPeriodoTemporale = value
EQNSeismic.shared.periodoTemporale = value.value
EQNSeismic.shared.saveFilters()
}
} else if indexPath.row == RowIdentifier.sismiFortiDistanza.rawValue {
cell.isDisabled = !currentSismiFortiAbilitati
cell.configureSlider(with: dataSourceSismiForti, current: currentSismiFortiDistanza)
cell.valueChanged = { [unowned self] value in
currentSismiFortiDistanza = value
EQNSeismic.shared.sismiFortiMagnitudo = value.value
EQNSeismic.shared.saveFilters()
case .magnitudoMinima:
cell.isDisabled = !isFilterInRadiusEnabled
cell.isUserInteractionEnabled = isFilterInRadiusEnabled
cell.configureSlider(with: dataSourceMinimumMagnitude, current: currentMinimumMagnitude)
cell.valueChanged = { [weak self] value in
self?.onChangeMinimumMagnitude(value)
}
default:
break
}
return cell
@@ -155,30 +127,37 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
cell.titleLabel.text = setting.displayTitle
cell.detailTextLabel?.text = setting.subtitle
if indexPath.row == RowIdentifier.sismiFortiAbilita.rawValue {
cell.toggleSwitch.isOn = currentSismiFortiAbilitati
cell.valueChanged = { [unowned self] value in
currentSismiFortiAbilitati = value
EQNSeismic.shared.sismiFortiAbilitati = value
EQNSeismic.shared.saveFilters()
loadDataSource()
tableView.reloadData()
switch identifier {
case .sismiNelRaggio:
let isCurrentFilter = currentFilterType == .inRadius
cell.isDisabled = !isLocationAvailable
cell.toggleSwitch.isOn = isCurrentFilter
cell.valueChanged = { [weak self] enabled in
self?.onChangeFilterOption(enabled, filter: .inRadius)
}
} else if indexPath.row == RowIdentifier.sismiQualsiasiMagnitudo.rawValue {
cell.toggleSwitch.isOn = currentSismiQualsiasiMagnitudo
cell.valueChanged = { [unowned self] value in
currentSismiQualsiasiMagnitudo = value
EQNSeismic.shared.sismiQualsiasiAbilitati = value
EQNSeismic.shared.saveFilters()
cell.errorLabel.text = !isLocationAvailable ? NSLocalizedString("filter_nolocation", comment: "") : nil
case .sismiRilevanti:
let isCurrentFilter = currentFilterType == .positionRelevant
cell.isDisabled = !isLocationAvailable
cell.toggleSwitch.isOn = isCurrentFilter
cell.valueChanged = { [weak self] enabled in
self?.onChangeFilterOption(enabled, filter: .positionRelevant)
}
} else if indexPath.row == RowIdentifier.modificaImpostazioni.rawValue {
cell.toggleSwitch.isOn = currentModificaImpostazioni
cell.valueChanged = { [unowned self] value in
currentModificaImpostazioni = value
EQNSeismic.shared.modificaImpostazioniAbilitato = value
EQNSeismic.shared.saveFilters()
cell.errorLabel.text = !isLocationAvailable ? NSLocalizedString("filter_nolocation", comment: "") : nil
case .sismiTutti:
let isCurrentFilter = currentFilterType == .worldWide
cell.toggleSwitch.isOn = isCurrentFilter
cell.valueChanged = { [weak self] enabled in
self?.onChangeFilterOption(enabled, filter: .worldWide)
}
case .sismiPercepiti:
let isCurrentFilter = currentFilterType == .userFelt
cell.toggleSwitch.isOn = isCurrentFilter
cell.valueChanged = { [weak self] enabled in
self?.onChangeFilterOption(enabled, filter: .userFelt)
}
default:
break
}
return cell
@@ -191,41 +170,34 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
@IBAction func exitTapped(_ sender: UIButton) {
// data needs to be re-downloaded if (or conditions):
// a) new magnitude is lower than the previous one and new value is less than 2.0
// b) show any near earthquake is active and value is changed
if let initialMagnitude = Float(initialMagnitudoMinima?.value ?? "10.0"), let currentMagnitude = Float(currentMagnitudoMinima.value) {
needsDataUpdate = currentMagnitude < 2.0 && initialMagnitude > currentMagnitude
}
if let initialQualsiasiMagnitudo = initialQualsiasiMagnitudo, currentSismiQualsiasiMagnitudo == true, initialQualsiasiMagnitudo != currentSismiQualsiasiMagnitudo {
needsDataUpdate = true
}
// a) filter type is changed
needsDataUpdate = initialFilterType != currentFilterType
delegate?.seismicFiltersControllerDidUpdateFilters(self)
updateNotificationSettingsIfNeeded()
dismiss(animated: true, completion: nil)
}
// MARK: - Private
private func showWarningAlertIfNeeded(for value: EQNGenericValue) {
guard let magnitude = Double(value.value), magnitude < 2.0 else { return }
private func onChangeFilterOption(_ enabled: Bool, filter: EQNSeismic.FilterType) {
currentFilterType = filter
EQNSeismic.shared.filterOption = filter
EQNSeismic.shared.saveFilters()
let alert = UIAlertController(title: NSLocalizedString("attention", comment: ""), message: NSLocalizedString("options_low_magnitude", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("main_understood", comment: ""), style: .default, handler: nil))
present(alert, animated: true, completion: nil)
loadDataSource()
tableView.reloadData()
}
private func onChangeMaximumDistance(_ item: EQNGenericValue) {
currentMaximumDistance = item
EQNSeismic.shared.maximumDistance = item.value
EQNSeismic.shared.saveFilters()
}
private func updateNotificationSettingsIfNeeded() {
// if the switch is enabled, update also the settings notification
guard currentModificaImpostazioni == true else { return }
// update notification settings with current filters
EQNNotificheReteSismiche.shared().energiaSisma = EQNSeismic.shared.magnitudoMinima;
EQNNotificheReteSismiche.shared().distanzaPosizione = EQNSeismic.shared.distanzaMassima
EQNNotificheReteSismiche.shared().isAbilitaVicini = EQNSeismic.shared.sismiQualsiasiAbilitati
EQNNotificheReteSismiche.shared().isTerremortiForti = EQNSeismic.shared.sismiFortiAbilitati
EQNNotificheReteSismiche.shared().energiaTerremotiForti = EQNSeismic.shared.sismiFortiMagnitudo
private func onChangeMinimumMagnitude(_ item: EQNGenericValue) {
currentMinimumMagnitude = item
EQNSeismic.shared.minimumMagnitude = item.value
EQNSeismic.shared.saveFilters()
}
}
@@ -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,9 @@
//
// SeismicNetworkData.swift
// Earthquake Network
//
// Created by Andrea Busi on 31/01/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import Foundation
@@ -0,0 +1,108 @@
//
// SeismicNetworkScrollIndicatorView.swift
// Earthquake Network
//
// Created by Andrea Busi on 31/01/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import UIKit
import CoreGraphics
class SeismicNetworkScrollIndicatorView: UIView {
private static let HighlightColor: UIColor = .red
var seismics: [SeismicNetworkViewModel] = [] {
didSet {
setNeedsDisplay()
}
}
var highlighted: SeismicNetworkViewModel? {
didSet {
setNeedsDisplay()
}
}
private var numberOfRectangles: Int {
seismics.count
}
// MARK: - View Lifecycle
override func draw(_ rect: CGRect) {
guard numberOfRectangles > 0 else { return }
let context = UIGraphicsGetCurrentContext()
let rectStandardWidth = rect.width
let rectStandardHeight = rect.height / CGFloat(numberOfRectangles)
let rectHighlightedMinHeight: CGFloat = 4
let smallRectangles = rectStandardHeight < 10
let highlightIndex = seismics.firstIndex(where: { $0 == highlighted }) ?? 100_000
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 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
if highlightIndex == index {
// Stiamo disegnando il sisma evidenziato.
// Valutiamo se utilizzare l'altezza minima.
let rectHeight = smallRectangles ? rectHighlightedMinHeight : rectStandardHeight
let yPosition = CGFloat(index) * rectStandardHeight
let rectangle = CGRect(x: 0, y: yPosition, width: rectStandardWidth, height: rectHeight)
let fillColor = smallRectangles ? Self.HighlightColor : seismic.colors.textColor.withAlphaComponent(0.3)
context?.setFillColor(fillColor.cgColor)
context?.fill(rectangle)
if !smallRectangles {
// disegniamo il bordo solo se i rettangoli non sono piccoli
let borderWidth: CGFloat = 2.0
context?.setStrokeColor(Self.HighlightColor.cgColor)
context?.setLineWidth(borderWidth) // Spessore del bordo
context?.stroke(rectangle.insetBy(dx: borderWidth / 2, dy: borderWidth / 2)) // Evita che il bordo venga tagliato
}
} else {
// Stiamo disegnando i sismi non evidenziati, utilizziamo sempre l'altezza predefinita
// Dobbiamo eventualmente calcolare un offset aggiuntivo,
// perchè il sisma evidenziato ha un'altezza maggiore (se i rettangoli sono piccoli)
let rectHeight = rectStandardHeight
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)
context?.fill(rectangle)
if !smallRectangles {
// altrimenti un bordo grigio
let borderWidth: CGFloat = 0.5
context?.setStrokeColor(AppTheme.Colors.gray.cgColor)
context?.setLineWidth(borderWidth) // Spessore del bordo
context?.stroke(rectangle)
}
}
}
}
}
@@ -8,24 +8,75 @@
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 {
var place: String
var network: String
var isPreliminary: Bool
var magnitude: String
var depth: String
var time: String
var distance: String
var coordinate: String
var population: String
var smartphones: String
var users: String
private let seismic: EQNSisma
let place: String
let network: String
let isPreliminary: Bool
let magnitude: String
let depth: String
let time: String
let distance: String
let coordinate: String
let population: String
let smartphones: String
let users: String
let colors: MagnitudeColors
// MARK: - Init
init(seismic: EQNSisma) {
self.seismic = seismic
self.place = seismic.place
self.network = seismic.provider
@@ -38,7 +89,7 @@ struct SeismicNetworkViewModel {
self.depth = ""
} else {
self.magnitude = String(format: "%.1f%@", seismic.magnitude.doubleValue, seismic.magnitudeType)
self.depth = String(format: "%@ %.1f km", NSLocalizedString("official_depth", comment: ""), seismic.depth.doubleValue)
self.depth = String(format: "%.1f km", seismic.depth.doubleValue)
}
// we need to check agains null values, because sometimes WS returns invalid dates
@@ -56,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 {
@@ -69,23 +120,82 @@ struct SeismicNetworkViewModel {
} else {
self.users = ""
}
}
// 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
self.colors = calculateColors(for: seismic.magnitude.doubleValue)
}
}
extension SeismicNetworkViewModel: Equatable {
static func == (lhs: SeismicNetworkViewModel, rhs: SeismicNetworkViewModel) -> Bool {
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
}
@@ -0,0 +1,250 @@
//
// SeismicNetworksIntensityMapViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 27/02/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import UIKit
import MapKit
class SeismicNetworksIntensityMapViewController: EQNBaseMapViewController {
private let seismic: EQNSisma
private var shakemaps: [EQNShakemap] = []
private var pinStyle: MapPinStyle {
get { AppPreferences.shared.mapPinStyle }
set { AppPreferences.shared.mapPinStyle = newValue }
}
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(
seismic: EQNSisma
) {
self.seismic = seismic
super.init()
}
@MainActor required init?(coder: NSCoder) {
fatalError("Plase use init(seismic:) instead")
}
// MARK: - View Lifecycle
override func extraUI() {
super.extraUI()
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
}
override func configureUI() {
super.configureUI()
navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: .init(handler: { [weak self] _ in
self?.dismiss(animated: true)
}))
navigationItem.rightBarButtonItems = [
UIBarButtonItem(image: UIImage(named: "navbar-icon-screenshot"), primaryAction: .init(handler: { [weak self] _ in
self?.shareScreenshot()
})),
UIBarButtonItem(image: UIImage(named: "navbar-icon-pin-arrow"), primaryAction: .init(handler: { [weak self] _ in
self?.nextPinStyle()
})),
]
}
override func registerMapAnnotationViews() {
mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .full))
mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .light))
mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .circle))
}
override func loadDataSource() {
Task {
let result = try await APIService.shared.fetchShakemap(isoCode: seismic.isoCode)
elaborateShakemaps(result)
}
}
override func elaborateMapCenter() {
setMapCenter(for: seismic.coordinate, span: MKCoordinateSpan(latitudeDelta: 2, longitudeDelta: 2))
}
// MARK: - Private
private func elaborateShakemaps(_ shakemaps: [EQNShakemap]) {
self.shakemaps = shakemaps
var shakemapPolyline = [MKPolyline]()
var shakemapAnnotations: [MKAnnotation] = []
for shakemap in shakemaps {
// create coordinates for current shakemap
let coordinates = zip(shakemap.lat, shakemap.lon).map { lat, lon in
CLLocationCoordinate2D(latitude: Double(lat) / 10_000.0, longitude: Double(lon) / 10_000.0)
}
let intensityColors = getColors(for: shakemap.intensity)
// create line to show on map
let polyline = ShakemapPolyline(coordinates: coordinates, count: coordinates.count)
polyline.intensity = shakemap.intensity
polyline.intensityColor = intensityColors.lineColor
shakemapPolyline.append(polyline)
// create annotation to show on top of the line
let middlePoint = coordinates[coordinates.count / 2]
let annotation = EQNMapAnnotationShakemap(coordinate: middlePoint, shakemap: shakemap)
annotation.intensityColor = intensityColors.lineColor
annotation.intensityTextColor = intensityColors.textColor
shakemapAnnotations.append(annotation)
}
let seismicAnnotation = EQNMapAnnotationSeismic(seismic: seismic)
shakemapAnnotations.append(seismicAnnotation)
// draw lines
mapView.addOverlays(shakemapPolyline)
updateMap(with: shakemapAnnotations)
}
private func nextPinStyle() {
pinStyle.next()
reloadMap()
}
private func shareScreenshot() {
let screenshot = createSnapshot(prepare: {
descriptionView.isHidden = true
}, restore: {
descriptionView.isHidden = false
})
let controller = UIActivityViewController(activityItems: [screenshot], applicationActivities: [])
present(controller, animated: true)
}
// MARK: - MKMapViewDelegate
override func setupAnnotationView(for annotation: MKAnnotation, on mapView: MKMapView) -> MKAnnotationView? {
switch annotation {
case let shakemapAnnotation as EQNMapAnnotationShakemap:
return shakemapAnnotation.toAnnotationView(mapView: mapView, style: .light)
case let seismicAnnotation as EQNMapAnnotationSeismic:
return seismicAnnotation.toAnnotationView(mapView: mapView, style: pinStyle)
default:
return nil
}
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let polyline = overlay as? ShakemapPolyline {
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = polyline.intensityColor
renderer.lineWidth = 6.0
return renderer
}
return MKOverlayRenderer()
}
private func getColors(for intensity: Float) -> (textColor: UIColor, lineColor: UIColor) {
let shakemapColors: [String] = [
"#3E26A8","#3E27AC","#3F28AF","#3F29B2","#402AB4","#402BB7","#412CBA","#412DBD","#422EBF","#422FC2",
"#4330C5","#4331C8","#4332CA","#4433CD","#4434D0","#4535D2","#4537D5","#4538D7","#4639D9","#463ADC",
"#463BDE","#463DE0","#473EE1","#473FE3","#4741E5","#4742E6","#4744E8","#4745E9","#4746EB","#4848EC",
"#4849ED","#484BEE","#484CF0","#484EF1","#484FF2","#4850F3","#4852F4","#4853F5","#4854F6","#4756F7",
"#4757F7","#4759F8","#475AF9","#475BFA","#475DFA","#465EFB","#4660FB","#4661FC","#4562FC","#4564FD",
"#4465FD","#4367FD","#4368FE","#426AFE","#416BFE","#406DFE","#3F6EFF","#3E70FF","#3C71FF","#3B73FF",
"#3974FF","#3876FE","#3677FE","#3579FD","#337AFD","#327CFC","#317DFC","#307FFB","#2F80FA","#2F82FA",
"#2E83F9","#2E84F8","#2E86F8","#2E87F7","#2D88F6","#2D8AF5","#2D8BF4","#2D8CF3","#2D8EF2","#2C8FF1",
"#2C90F0","#2B91EF","#2A93EE","#2994ED","#2895EC","#2797EB","#2798EA","#2699E9","#269AE8","#259BE8",
"#259CE7","#249EE6","#249FE5","#23A0E5","#23A1E4","#22A2E4","#21A3E3","#20A5E3","#1FA6E2","#1EA7E1",
"#1DA8E1","#1DA9E0","#1CAADF","#1BABDE","#1AACDD","#19ADDC","#17AEDA","#16AFD9","#14B0D8","#12B1D6",
"#10B2D5","#0EB3D4","#0BB3D2","#08B4D1","#06B5CF","#04B6CE","#02B7CC","#01B7CA","#00B8C9","#00B9C7",
"#00BAC6","#01BAC4","#02BBC2","#04BBC1","#06BCBF","#09BDBD","#0DBDBC","#10BEBA","#14BEB8","#17BFB6",
"#1AC0B5","#1DC0B3","#20C1B1","#23C1AF","#25C2AE","#27C2AC","#29C3AA","#2BC3A8","#2CC4A6","#2EC4A5",
"#2FC5A3","#31C5A1","#32C69F","#33C79D","#35C79B","#36C899","#38C896","#39C994","#3BC992","#3DCA90",
"#40CA8D","#42CA8B","#45CB89","#48CB86","#4BCB84","#4ECC81","#51CC7F","#54CC7C","#57CC7A","#5ACC77",
"#5ECD74","#61CD72","#64CD6F","#67CD6C","#6BCD69","#6ECD66","#72CD64","#76CC61","#79CC5E","#7DCC5B",
"#81CC59","#84CC56","#88CB53","#8BCB51","#8FCB4E","#93CA4B","#96CA48","#9AC946","#9DC943","#A1C840",
"#A4C83E","#A7C73B","#ABC739","#AEC637","#B2C635","#B5C533","#B8C431","#BBC42F","#BEC32D","#C2C32C",
"#C5C22A","#C8C129","#CBC128","#CEC027","#D0BF27","#D3BF27","#D6BE27","#D9BE28","#DBBD28","#DEBC29",
"#E1BC2A","#E3BC2B","#E6BB2D","#E8BB2E","#EABA30","#ECBA32","#EFBA35","#F1BA37","#F3BA39","#F5BA3B",
"#F7BA3D","#F9BA3E","#FBBB3E","#FCBC3E","#FEBD3D","#FEBE3C","#FEC03B","#FEC13A","#FEC239","#FEC438",
"#FEC537","#FEC735","#FEC834","#FECA33","#FDCB32","#FDCD31","#FDCE31","#FCD030","#FBD22F","#FBD32E",
"#FAD52E","#F9D62D","#F9D82C","#F8D92B","#F7DB2A","#F7DD2A","#F6DE29","#F6E028","#F5E128","#F5E327",
"#F5E526","#F5E626","#F5E825","#F5E924","#F5EB23","#F5EC22","#F5EE21","#F6EF20","#F6F11F","#F6F21E",
"#F7F41C","#F7F51B","#F8F71A","#F8F818","#F9F916","#F9FB15"
]
let minIntensity = shakemaps.map { $0.intensity }.min() ?? 0
let maxIntensity = shakemaps.map { $0.intensity }.max() ?? 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
return (textColor: textColor, lineColor: lineColor)
}
}
extension EQNMapAnnotationShakemap {
func toAnnotationView(
mapView: MKMapView,
style: MapPinStyle,
isUserSelection: Bool = false
) -> MKAnnotationView? {
switch style {
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.magnitudeTextColor = intensityTextColor ?? .black
annotationView.magnitudeBackgroundColor = intensityColor
annotationView.canShowCallout = true
return annotationView
case .circle:
return nil
}
}
}
fileprivate class ShakemapPolyline: MKPolyline {
var intensity: Float = 0
var intensityColor: UIColor = .white
}
@@ -15,11 +15,18 @@ protocol SeismicNetworksMapDetailViewControllerDelegate: AnyObject {
}
class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
private var pinStyle: MapPinStyle {
get { AppPreferences.shared.mapPinStyle }
set { AppPreferences.shared.mapPinStyle = newValue }
}
private let eqnSeismic = EQNSeismic.shared
// MARK: - State
override var isCloseButtonVisible: Bool { false }
override var isFilterViewVisible: Bool {
// a custom filter view id displayed
// a custom filter view is displayed
true
}
weak var delegate: SeismicNetworksMapDetailViewControllerDelegate?
@@ -42,10 +49,7 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
seismicsFilterLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
seismicsFilterLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8.0).isActive = true
seismicsFilterLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8.0).isActive = true
// tap recognizer
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(filtersTapped(_:)))
view.addGestureRecognizer(tapRecognizer)
return view
}()
@@ -76,6 +80,24 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
fatalError("init(coder:) is not available, please use init(seismic:allSeismics:)")
}
// MARK: - View Lifecycle
override func configureUI() {
super.configureUI()
navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: .init(handler: { [weak self] _ in
self?.dismiss(animated: true)
}))
navigationItem.rightBarButtonItems = [
UIBarButtonItem(image: UIImage(named: "navbar-icon-screenshot"), primaryAction: .init(handler: { [weak self] _ in
self?.shareScreenshot()
})),
UIBarButtonItem(image: UIImage(named: "navbar-icon-pin-arrow"), primaryAction: .init(handler: { [weak self] _ in
self?.nextPinStyle()
})),
]
}
// MARK: - Public
func updateSeismics(_ seismics: [EQNSisma]) {
@@ -84,7 +106,9 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
}
override func registerMapAnnotationViews() {
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.DoubleLineIdentifier)
mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .full))
mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .light))
mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .circle))
}
override func loadDataSource() {
@@ -92,14 +116,14 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
updateMap(with: annotations)
// if the given seismic is still in the data source, show circles
// otherwise just remove any other circles already on the map
if allSeismics.contains(seismic) {
addCircles(for: seismic.coordinate)
// if the filter is "in radius",
// show a circle with selected radius
if eqnSeismic.filterOption == .inRadius, let distance = Double(eqnSeismic.maximumDistance) {
addCircle(center: EQNUser.default().lastPosition, radius: distance * 1_000)
} else {
addCircles(for: nil)
addCircle(center: nil, radius: 0)
}
loadFiltersRecap()
}
@@ -127,31 +151,61 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
present(alert, animated: true)
}
// MARK: - Private
private func loadFiltersRecap() {
let filters = FiltersViewModel()
let recap = "\(NSLocalizedString("filter_filter", comment: "")): "
+ "M≥\(filters.magnitude) "
+ "D≤\(filters.distance) "
+ "T≤\(filters.timeframe)"
seismicsFilterLabel.text = recap
override func zPriority(for annotation: MKAnnotation) -> MKAnnotationViewZPriority {
guard let annotation = annotation as? EQNMapAnnotationSeismic else {
return .min
}
// il sisma cliccato dall'utente sta sopra a tutti
if annotation.seismic == seismic {
return .max
}
// Ordiniamo le annotazioni in base all amagnitudo, quelle con valore maggiore devono stare sopra.
// La `zPriority` viene calcolata utilizzando la posizione nella lista
let index = mapAnnotations
.compactMap { $0 as? EQNMapAnnotationSeismic }
.sorted(by: { $0.seismic.magnitude.doubleValue < $1.seismic.magnitude.doubleValue })
.firstIndex(where: { $0 == annotation })
guard let index else {
return .min
}
let priority = Float(index) / Float(mapAnnotations.count)
return .init(priority)
}
private func addCircles(for location: CLLocation?) {
// MARK: - Private
private func nextPinStyle() {
pinStyle.next()
reloadMap()
}
private func loadFiltersRecap() {
let filter = EQNSeismic.shared.filterOption
let text = switch filter {
case .inRadius: NSLocalizedString("filter_area", comment: "")
case .positionRelevant: NSLocalizedString("filter_relevant", comment: "")
case .worldWide: NSLocalizedString("filter_all", comment: "")
case .userFelt: NSLocalizedString("filter_felt", comment: "")
}
seismicsFilterLabel.text = text
}
private func addCircle(
center location: CLLocation?,
radius: Double
) {
// remove any previous circles
mapView.removeOverlays(mapCircles)
mapCircles.removeAll()
// aggiungiamo 3 cerchi concentrici per il punto specificato (se disponibile)
// li inseriamo a a 50, 100 e 200 km.
guard let location = location else { return }
let circles = [
MKCircle(center: location.coordinate, radius: 25_000),
MKCircle(center: location.coordinate, radius: 100_000),
MKCircle(center: location.coordinate, radius: 200_000)
MKCircle(center: location.coordinate, radius: radius),
]
// !!note: is important to assign here the circles
@@ -162,16 +216,13 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
mapView.addOverlays(circles)
}
// MARK: - Actions
@objc override func filtersTapped(_ sender: UIGestureRecognizer) {
let controller = SeismicFiltersViewController.makeViewController()
controller.delegate = self
controller.modalPresentationStyle = .overCurrentContext
controller.modalTransitionStyle = .crossDissolve
present(controller, animated: true, completion: nil)
private func shareScreenshot() {
let screenshot = createSnapshot()
let controller = UIActivityViewController(activityItems: [screenshot], applicationActivities: [])
present(controller, animated: true)
}
// MARK: - Map
override func setupAnnotationView(for annotation: MKAnnotation, on mapView: MKMapView) -> MKAnnotationView? {
@@ -179,15 +230,8 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
return nil
}
let viewModel = SeismicNetworkViewModel(seismic: annotation.seismic)
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: EQNCustomAnnotationView.DoubleLineIdentifier, for: annotation) as! EQNCustomAnnotationView
annotationView.image = annotation.image
annotationView.title = annotation.title
annotationView.subtitle = viewModel.magnitude
return annotationView
let isUserSelection = annotation.seismic == seismic
return annotation.toAnnotationView(mapView: mapView, style: pinStyle, isUserSelection: isUserSelection)
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
@@ -196,12 +240,39 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
}
let circle = MKCircleRenderer(overlay: circleOverlay)
circle.strokeColor = AppTheme.Colors.darkGray
circle.lineWidth = 1.0
circle.strokeColor = AppTheme.Colors.red
circle.lineWidth = 2.0
return circle
}
}
extension EQNMapAnnotationSeismic {
func toAnnotationView(
mapView: MKMapView,
style: MapPinStyle,
isUserSelection: Bool = false
) -> MKAnnotationView {
switch style {
case .full, .light:
let identifier = EQNSeismicAnnotationView.identifier(for: style)
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier, for: self) as! EQNSeismicAnnotationView
annotationView.title = self.title
annotationView.subtitle = self.subtitle
annotationView.magnitude = String(format: "M%.1f", self.seismic.magnitude.doubleValue)
annotationView.magnitudeTextColor = self.textColor
annotationView.magnitudeBackgroundColor = .white
annotationView.isUserSelection = isUserSelection
return annotationView
case .circle:
let identifier = EQNSeismicAnnotationView.identifier(for: style)
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier, for: self) as! EQNSeismicAnnotationView
annotationView.image = image(height: EQNSeismicAnnotationView.CircleViewHeight,
isUserSelection: isUserSelection)
return annotationView
}
}
}
extension SeismicNetworksMapDetailViewController: SeismicFiltersViewControllerDelegate {
func seismicFiltersControllerDidUpdateFilters(_ controller: SeismicFiltersViewController) {
delegate?.seismicNetworksMapDetailControllerWillUpdateData(self, needsDataUpdate: controller.needsDataUpdate)
@@ -15,23 +15,23 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
private enum CellType {
case seismic(EQNSisma)
case advertise(GADNativeAd)
case advertise(NativeAd)
}
enum CardDisplayType: Int, CaseIterable {
case small
case full
case minimal
}
private static let SegueIdentifierFilters = "ShowFilters"
private static let SegueIdentifierSettings = "ShowSettings"
private static let SegueIdentifierSeismicNetworks = "ShowSeismicNetworks"
private static let SegueIdentifierCardSettings = "ShowCardSettings"
// MARK: - Internal
@IBOutlet private weak var tableView: UITableView?
@IBOutlet private weak var expandeCollapseButton: UIBarButtonItem!
weak var currentMapController: SeismicNetworksMapDetailViewController?
/// The ad loader
private lazy var adLoader: GADAdLoader = {
let adLoader = GADAdLoader(
private lazy var adLoader: AdLoader = {
let adLoader = AdLoader(
adUnitID: EQNAdMobAppIdNativeBanner, rootViewController: self,
adTypes: [.native], options: nil)
adLoader.delegate = self
@@ -40,25 +40,88 @@ 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
private var openedPushNotification: EQNOfficialPushNotification? {
didSet {
scrollToOpenedSeismic = true
}
}
private var scrollToOpenedSeismic = false
/// Current displayed controller with map
private weak var currentMapController: SeismicNetworksMapDetailViewController?
/// Keep track of the current cell at the center
private var currentCenteredIndexPath: IndexPath?
// MARK: - UI
@IBOutlet private weak var displayModeButton: UIBarButtonItem!
@IBOutlet private weak var sortButton: UIBarButtonItem!
private var tableViewTopConstraint: NSLayoutConstraint?
private lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.dataSource = self
tableView.showsVerticalScrollIndicator = false
return tableView
}()
private lazy var filterChangedView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = AppTheme.Colors.pureBlue
view.addSubview(filterChangedLabel)
filterChangedLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8.0).isActive = true
filterChangedLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8.0).isActive = true
filterChangedLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0).isActive = true
filterChangedLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8.0).isActive = true
return view
}()
private lazy var filterChangedLabel: UILabel = {
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .white
label.font = .preferredFont(forTextStyle: .subheadline)
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
private lazy var scrollIndicatorView: SeismicNetworkScrollIndicatorView = {
let view = SeismicNetworkScrollIndicatorView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear
view.layer.borderColor = AppTheme.Colors.gray.cgColor
return view
}()
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
configureUI()
checkForLocation()
refreshUI()
// only the first time, show the popup for country selection
let alreadyPresented = UserDefaults.standard.bool(forKey: EQNUserDefaultKeyOneShotShowCountry)
if !alreadyPresented {
performSegue(withIdentifier: Self.SegueIdentifierSettings, sender: nil)
UserDefaults.standard.setValue(true, forKey: EQNUserDefaultKeyOneShotShowCountry)
}
configureFilterView(isVisible: false)
NotificationCenter.default.addObserver(self, selector: #selector(didReceiveDownloadCompleteNotification(_:)), name: .EQNDownloadDataDidComplete, object: nil)
}
@@ -67,16 +130,103 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
super.viewWillAppear(animated)
loadData(forced: false)
// check for a push to manage
if let notification = EQNOfficialPushNotification.stored() {
manageFilter(for: notification)
self.openedPushNotification = notification
EQNOfficialPushNotification.removeStored()
} else {
configureFilterView(isVisible: false)
self.openedPushNotification = nil
}
tableView.reloadData()
}
// MARK: - Private
private func setupUI() {
view.addSubview(scrollIndicatorView)
view.addSubview(tableView)
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.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
}
private func setupFilterView(isVisible: Bool) {
if isVisible && filterChangedView.superview == nil {
view.addSubview(filterChangedView)
tableViewTopConstraint?.isActive = false
filterChangedView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
filterChangedView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
filterChangedView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableViewTopConstraint = filterChangedView.bottomAnchor.constraint(equalTo: tableView.topAnchor)
tableViewTopConstraint?.isActive = true
} else {
filterChangedView.removeFromSuperview()
tableViewTopConstraint?.isActive = false
tableViewTopConstraint = tableView.topAnchor.constraint(equalTo: view.topAnchor)
tableViewTopConstraint?.isActive = true
}
}
private func configureFilterView(isVisible: Bool) {
setupFilterView(isVisible: isVisible)
if isVisible {
filterChangedLabel.text = NSLocalizedString("official_filter_changed", comment: "")
filterChangedView.backgroundColor = AppTheme.Colors.pureBlue
} else {
filterChangedLabel.text = nil
filterChangedView.backgroundColor = .white
}
}
private func configureUI() {
title = NSLocalizedString("tab_official", comment: "").capitalized
tableView?.estimatedRowHeight = 300.0
tableView?.rowHeight = UITableView.automaticDimension
tableView?.register(SeismicNetworkTableViewCell.self, forCellReuseIdentifier: SeismicNetworkTableViewCell.Identifier)
tableView?.register(SeismicNetworkAdvertiseTableViewCell.self, forCellReuseIdentifier: SeismicNetworkAdvertiseTableViewCell.Identifier)
tableView?.emptyDataSetSource = self
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
tableView.contentInset = EQNBaseContainerTableViewCell.EdgeInsets
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)
}
])
}
private func checkForLocation() {
// check if a valid location is available,
// otherwise change the filter settings
if !isLocationAvailable() {
EQNSeismic.shared.filterOption = .worldWide
EQNSeismic.shared.saveFilters()
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
@@ -85,14 +235,6 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
if let controller = segue.destination as? SeismicFiltersViewController {
controller.delegate = self
}
case Self.SegueIdentifierSettings:
if let controller = segue.destination as? SeismicSettingsViewController {
controller.delegate = self
}
case Self.SegueIdentifierSeismicNetworks:
if let navController = segue.destination as? UINavigationController, let controller = navController.viewControllers.first as? SeismicSettingsNetworksViewController {
controller.delegate = self
}
case Self.SegueIdentifierCardSettings:
if let controller = segue.destination as? SeismicCardSettingsViewController {
controller.delegate = self
@@ -106,11 +248,18 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
let seismics = getSeismics()
let controller = SeismicNetworksMapDetailViewController(seismic: seismic, allSeismics: seismics)
controller.delegate = self
present(controller, animated: true, completion: nil)
let navController = UINavigationController(rootViewController: controller)
present(navController, animated: true, completion: nil)
self.currentMapController = controller
}
private func showIntensityMap(for seismic: EQNSisma) {
let controller = SeismicNetworksIntensityMapViewController(seismic: seismic)
let navController = UINavigationController(rootViewController: controller)
present(navController, animated: true)
}
// MARK: - Notifications
@objc func didReceiveDownloadCompleteNotification(_ sender: Notification) {
@@ -126,21 +275,26 @@ 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) }
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")
}
if informations.contains(.buttons) {
expandeCollapseButton.image = UIImage(named: "navbar-icon-arrow-collapse")
} else {
expandeCollapseButton.image = UIImage(named: "navbar-icon-arrow-expand")
}
tableView.reloadData()
updateCenterCellIndexPath()
tableView?.reloadData()
if scrollToOpenedSeismic, let index = getSeismics().firstIndex(where: { isSeismicToHighlight(seismic: $0) }) {
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
scrollToOpenedSeismic = false
}
}
private func loadAd() {
adLoader.load(GADRequest())
adLoader.load(Request())
}
private func loadData(forced: Bool) {
@@ -152,7 +306,8 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
let allSeismics = EQNManager.manager().retiSismiche
let filteredSeismics = EQNSeismic.shared.filterSeismicList(allSeismics ?? [])
rows = filteredSeismics.map { .seismic($0) }
seismicViewModels = filteredSeismics.map(SeismicNetworkViewModel.init)
#if ADS_ENABLED
// if is not a pro user, show an advertise
if !EQNPurchaseUtility.isProVersionEnabled() {
@@ -164,6 +319,9 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
if let mapController = currentMapController {
mapController.updateSeismics(filteredSeismics)
}
scrollIndicatorView.seismics = seismicViewModels
currentCenteredIndexPath = nil
}
private func getSeismics() -> [EQNSisma] {
@@ -176,6 +334,307 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
return seismics
}
private func changeSort(to sort: EQNSeismic.Sort) {
EQNSeismic.shared.sort = sort
EQNSeismic.shared.saveFilters()
setupSortMenu()
refreshUI()
}
private func manageFilter(
for notification: EQNOfficialPushNotification
) {
//gestisco i filtri solo se la posizione dell'utente è nota
guard let userPosition = EQNUser.default().lastPosition else {
return
}
var filter_type = EQNSeismic.shared.filterOption
var filter_radius = Double(EQNSeismic.shared.maximumDistance) ?? 0
var filter_min_magnitude = Double(EQNSeismic.shared.minimumMagnitude) ?? 0
var filter_changed = false
//recupero i dati del sisma notificato
let notification_magnitude = notification.magnitude
let notification_latitude = notification.coordinate.coordinate.latitude
let notification_longitude = notification.coordinate.coordinate.longitude
//distanza tra smartphone utente e sisma notificato
let locationNotification = CLLocation(latitude: notification_latitude, longitude: notification_longitude)
let distance = userPosition.distance(from: locationNotification) / 1_000
//verifico se il sisma è significativo in base alla definizione di significativo
var is_significant = true
if notification_magnitude < 7.0 && distance > 2000 {
is_significant = false
} else if notification_magnitude < 6.5 && distance > 1600 {
is_significant = false
} else if notification_magnitude < 6.0 && distance > 1300 {
is_significant = false
} else if notification_magnitude < 5.5 && distance > 1000 {
is_significant = false
} else if notification_magnitude < 5.0 && distance > 700 {
is_significant = false
} else if notification_magnitude < 4.5 && distance > 500 {
is_significant = false
} else if notification_magnitude < 4.0 && distance > 350 {
is_significant = false
} else if notification_magnitude < 3.5 && distance > 200 {
is_significant = false
} else if notification_magnitude < 3.0 && distance > 125 {
is_significant = false
} else if notification_magnitude < 2.5 && distance > 70 {
is_significant = false
} else if notification_magnitude < 2.0 && distance > 35 {
is_significant = false
} else if notification_magnitude < 1.5 && distance > 20 {
is_significant = false
}
//verifico se devo modificare il filtro scelto dall'utente
if filter_type == .inRadius { //filter_type=0 è il filtro basato su raggio e magnitudo
if distance > 2000 && is_significant {
filter_type = .positionRelevant //passo al filtro che mostra i sismi significativi (perché il raggio massimo del filtro basato sul raggio è 2000)
updateFilter(type: filter_type)
filter_changed = true
}
else if distance > 2000 && notification_magnitude >= 2.0 {
filter_type = .worldWide //passo al filtro che mostra tutti i sismi nel mondo
updateFilter(type: filter_type)
filter_changed = true
}
else {
//verifico se devo cambiare il raggio del filtro
if distance > filter_radius {
if distance > 1500 {
filter_radius = 2000
} else if distance > 1000 {
filter_radius = 1500
} else if distance > 750 {
filter_radius = 1000
} else if distance > 500 {
filter_radius = 750
} else if distance > 250 {
filter_radius = 500
} else if distance > 100 {
filter_radius = 250
}
updateFilter(radius: filter_radius)
filter_changed = true
}
//verifico se devo cambiare la mgnitudo del filtro
if notification_magnitude < filter_min_magnitude {
if notification_magnitude < 1.0 {
filter_min_magnitude = 0.0
} else if notification_magnitude < 2.0 {
filter_min_magnitude = 1.0
} else if notification_magnitude < 3.0 {
filter_min_magnitude = 2.0
} else if notification_magnitude < 4.0 {
filter_min_magnitude = 3.0
} else if notification_magnitude < 5.0 {
filter_min_magnitude = 4.0
} else if notification_magnitude < 6.0 {
filter_min_magnitude = 5.0
}
filter_changed = true
updateFilter(magnitude: filter_min_magnitude)
}
}
}
if filter_type == .positionRelevant && !is_significant && distance <= 2000 {
filter_type = .inRadius //passo a filtro basato su raggio e magnitudo
updateFilter(type: filter_type)
if distance > filter_radius {
if distance>1500 {
filter_radius = 2000
}
else if distance > 1000 {
filter_radius = 1500
}
else if distance > 750 {
filter_radius = 1000
}
else if distance > 500 {
filter_radius = 750
}
else if distance > 250 {
filter_radius = 500
}
else if distance > 100 {
filter_radius = 250
}
updateFilter(radius: filter_radius)
}
if notification_magnitude < filter_min_magnitude {
if notification_magnitude < 1.0 {
filter_min_magnitude = 0.0
}
else if notification_magnitude < 2.0 {
filter_min_magnitude = 1.0
}
else if notification_magnitude < 3.0 {
filter_min_magnitude = 2.0
}
else if notification_magnitude < 4.0 {
filter_min_magnitude = 3.0
}
else if notification_magnitude < 5.0 {
filter_min_magnitude = 4.0
}
else if notification_magnitude < 6.0 {
filter_min_magnitude = 5.0
}
updateFilter(magnitude: filter_min_magnitude)
}
filter_changed = true
}
if filter_type == .positionRelevant && !is_significant && distance > 2000 && notification_magnitude >= 2.0 {
filter_type = .worldWide //passo a filtro che mostra tutti i sismi nel mondo
updateFilter(type: filter_type)
filter_changed = true
}
if filter_type == .worldWide && notification_magnitude < 2.0 && is_significant {
filter_type = .positionRelevant //passo a filtro sismi significativi
updateFilter(type: filter_type)
filter_changed = true
}
if filter_type == .worldWide && notification_magnitude < 2.0 && distance <= 2000 && !is_significant {
filter_type = .inRadius
updateFilter(type: filter_type)
if distance > filter_radius {
if distance > 1500 {
filter_radius = 2000
}
else if distance > 1000 {
filter_radius = 1500
}
else if distance > 750 {
filter_radius = 1000
}
else if distance > 500 {
filter_radius = 750
}
else if distance > 250 {
filter_radius = 500
}
else if distance > 100 {
filter_radius = 250
}
updateFilter(radius: filter_radius)
}
if notification_magnitude < filter_min_magnitude {
if notification_magnitude < 1.0 {
filter_min_magnitude = 0.0
}
else if notification_magnitude < 2.0 {
filter_min_magnitude = 1.0
}
else if notification_magnitude < 3.0 {
filter_min_magnitude = 2.0
}
else if notification_magnitude < 4.0 {
filter_min_magnitude = 3.0
}
else if notification_magnitude < 5.0 {
filter_min_magnitude = 4.0
}
else if notification_magnitude < 6.0 {
filter_min_magnitude = 5.0
}
updateFilter(magnitude: filter_min_magnitude)
}
filter_changed = true
}
//mostro all'utente un messaggio per avvisarlo che i filtri sono stati modificati
configureFilterView(isVisible: filter_changed)
if filter_changed {
loadData(forced: true)
}
}
private func updateFilter(
type: EQNSeismic.FilterType? = nil,
radius: Double? = nil,
magnitude: Double? = nil
) {
if let type {
EQNSeismic.shared.filterOption = type
}
if let radius {
EQNSeismic.shared.maximumDistance = String(format: "%.0f", radius)
}
if let magnitude {
EQNSeismic.shared.minimumMagnitude = String(format: "%.1f", magnitude)
}
EQNSeismic.shared.saveFilters()
}
private func isLocationAvailable() -> Bool {
EQNUser.default().lastPosition != nil
}
private func isSeismicToHighlight(seismic: EQNSisma) -> Bool {
guard let notification = openedPushNotification else {
return false
}
guard let seismicDate = seismic.date, let notificationDate = notification.date else { return false }
let deltaTime = abs(seismicDate.timeIntervalSince(notificationDate))
let magnitudeRatio = seismic.magnitude.doubleValue / notification.magnitude
let latitudeDiff = abs(seismic.coordinate.coordinate.latitude - notification.coordinate.coordinate.latitude)
let longitudeDiff = abs(seismic.coordinate.coordinate.longitude - notification.coordinate.coordinate.longitude)
if deltaTime <= 120 && magnitudeRatio > 0.8 && magnitudeRatio < 1.2 && latitudeDiff < 1 && longitudeDiff < 1 { // secondi?
return true
}
return false
}
private func getCenterCellIndexPath() -> IndexPath? {
let centerPoint = CGPoint(x: tableView.bounds.midX, y: tableView.bounds.midY)
if let indexPath = tableView.indexPathForRow(at: centerPoint) {
return indexPath
}
// Se il metodo diretto fallisce, cerchiamo la cella più vicina
if let visibleIndexPaths = tableView.indexPathsForVisibleRows {
return visibleIndexPaths.min(by: { (indexPath1, indexPath2) -> Bool in
let rect1 = tableView.rectForRow(at: indexPath1)
let rect2 = tableView.rectForRow(at: indexPath2)
let distance1 = abs(rect1.midY - centerPoint.y)
let distance2 = abs(rect2.midY - centerPoint.y)
return distance1 < distance2
})
}
return nil
}
private func updateCenterCellIndexPath() {
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]
}
}
}
// MARK: - Actions
@IBAction func refreshDataTapped(_ sender: Any) {
@@ -185,19 +644,19 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
@IBAction func openFilterTapped(_ sender: Any) {
performSegue(withIdentifier: Self.SegueIdentifierFilters, sender: nil)
}
@IBAction func openSettingsTapped(_ sender: Any) {
performSegue(withIdentifier: Self.SegueIdentifierSettings, sender: nil)
}
@IBAction func collapseExpandTapped(_ sender: Any) {
if informations.contains(.buttons) {
cardDisplayType.next()
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()
}
@@ -211,18 +670,28 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
let row = rows[indexPath.row]
switch row {
case .seismic(let seismic):
let cell = tableView.dequeueReusableCell(withIdentifier: SeismicNetworkTableViewCell.Identifier, for: indexPath) as! SeismicNetworkTableViewCell
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
}
cell.configure(with: seismic, type: type, informations: informations)
cell.delegate = self
return cell
case .advertise(let nativeAd):
let cell = tableView.dequeueReusableCell(withIdentifier: SeismicNetworkAdvertiseTableViewCell.Identifier, for: indexPath) as! SeismicNetworkAdvertiseTableViewCell
let cell = tableView.dequeueReusableCell(cellIdentifiable: SeismicNetworkAdvertiseTableViewCell.self, for: indexPath)
cell.loadNativeAd(nativeAd)
return cell
}
@@ -237,6 +706,12 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
}
}
// MARK: - UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateCenterCellIndexPath()
}
// MARK: - Private
private func openCalendar(for seismic: EQNSisma) {
@@ -292,25 +767,25 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
}
}
extension SeismicNetworksViewController: GADNativeAdLoaderDelegate {
func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADNativeAd) {
print("[GADAdLoader] didReceive")
extension SeismicNetworksViewController: NativeAdLoaderDelegate {
func adLoader(_ adLoader: AdLoader, didReceive nativeAd: NativeAd) {
print("[AdLoader] didReceive")
let adPosition = min(3, rows.count)
rows.insert(.advertise(nativeAd), at: adPosition)
tableView?.reloadData()
tableView.reloadData()
}
func adLoader(_ adLoader: GADAdLoader, didFailToReceiveAdWithError error: Error) {
func adLoader(_ adLoader: AdLoader, didFailToReceiveAdWithError error: Error) {
// nope
print("[GADAdLoader] didFailToReceiveAdWithError: \(error.localizedDescription)")
print("[AdLoader] didFailToReceiveAdWithError: \(error.localizedDescription)")
}
}
extension SeismicNetworksViewController: SeismicNetworkTableViewCellDelegate {
extension SeismicNetworksViewController: SeismicNetworkBaseTableViewCellDelegate {
func seismicNetworkCellDidTapShare(_ cell: SeismicNetworkTableViewCell) {
guard let index = tableView?.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
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
let snapshot = cell.contentView.createSnapshot()
@@ -326,38 +801,44 @@ extension SeismicNetworksViewController: SeismicNetworkTableViewCellDelegate {
present(controller, animated: true)
}
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkTableViewCell) {
guard let index = tableView?.indexPath(for: cell) else { return }
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell) else { return }
let indexToReloads = [openMapIndexPath, index].compactMap { $0 }
openMapIndexPath = index
tableView?.reloadRows(at: indexToReloads, with: .automatic)
tableView.reloadRows(at: indexToReloads, with: .automatic)
}
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkTableViewCell) {
guard let index = tableView?.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
showMapDetail(for: seismic)
}
func seismicNetworkCellDidTapCalendar(_ cell: SeismicNetworkTableViewCell) {
guard let index = tableView?.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
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: 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) {
guard let index = tableView?.indexPath(for: cell) else { return }
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell) else { return }
let indexToReloads = [openMapIndexPath, index].compactMap { $0 }
openMapIndexPath = nil
tableView?.reloadRows(at: indexToReloads, with: .automatic)
tableView.reloadRows(at: indexToReloads, with: .automatic)
}
}
@@ -375,24 +856,6 @@ extension SeismicNetworksViewController: SeismicFiltersViewControllerDelegate {
}
}
extension SeismicNetworksViewController: SeismicSettingsViewControllerDelegate {
func seismicSettingsControllerDidComplete(_ controller: SeismicSettingsViewController) {
// riscarichiamo i dati, le reti selezionate potrebbero essere cambiate
loadData(forced: true)
}
func seismicSettingsControllerWillOpenProviders(_ controller: SeismicSettingsViewController) {
performSegue(withIdentifier: Self.SegueIdentifierSeismicNetworks, sender: nil)
}
}
extension SeismicNetworksViewController: SeismicSettingsNetworksViewControllerDelegate {
func seismicSettingsNetworksControllerDidComplete(_ controller: SeismicSettingsNetworksViewController) {
// riscarichiamo i dati, le reti selezionate potrebbero essere cambiate
loadData(forced: true)
}
}
extension SeismicNetworksViewController: SeismicCardSettingsViewControllerDelegate {
func seismicCardSettingsDidComplete(_ controller: SeismicCardSettingsViewController) {
refreshUI()
@@ -400,9 +863,12 @@ extension SeismicNetworksViewController: SeismicCardSettingsViewControllerDelega
}
extension SeismicNetworksViewController: DZNEmptyDataSetSource {
func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! {
func title(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString? {
let text = EQNSeismic.shared.filterOption == .positionRelevant
? NSLocalizedString("filter_empty_relevant", comment: "")
: NSLocalizedString("filter_empty_area", comment: "")
let attributes = [ NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .body) ]
let string = NSAttributedString(string: NSLocalizedString("filter_empty", comment: ""), attributes: attributes)
let string = NSAttributedString(string: text, attributes: attributes)
return string
}
}
@@ -1,118 +0,0 @@
//
// SeismicSettingsNetworksViewController.swift
// Earthquake Network
//
// Created by Busi Andrea on 14/09/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
import UIKit
protocol SeismicSettingsNetworksViewControllerDelegate: AnyObject {
func seismicSettingsNetworksControllerDidComplete(_ controller: SeismicSettingsNetworksViewController)
}
class SeismicSettingsNetworksViewController: UITableViewController {
weak var delegate: SeismicSettingsNetworksViewControllerDelegate?
// MARK: - Private
private var networks = [EQNSeismicNetwork]()
private var savedNetworks = [String]()
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(SettingDetailTableViewCell.self, forCellReuseIdentifier: SettingDetailTableViewCell.Identifier)
tableView.register(SettingSectionHeaderView.self, forHeaderFooterViewReuseIdentifier: SettingSectionHeaderView.Identifier)
loadData()
}
// MARK: - Private
private func loadData() {
networks = EQNData.seismicNetworks().sorted(by: { $0.acronym < $1.acronym })
// load saved selected networks or fill with all available networks
let savedNetworks = EQNUserData.shared.seismicNetworksSelected()
if !savedNetworks.isEmpty {
self.savedNetworks = savedNetworks
} else {
self.savedNetworks = EQNData.seismicNetworkAcronyms()
}
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SettingSectionHeaderView.Identifier) as! SettingSectionHeaderView
headerView.titleLabel.text = NSLocalizedString("options_agencies", comment: "");
return headerView
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
CGFloat(SettingSectionHeaderView.Height)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
networks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let network = networks[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: SettingDetailTableViewCell.Identifier, for: indexPath) as! SettingDetailTableViewCell
cell.textLabel?.text = "\(network.acronym) (\(network.country))"
if savedNetworks.contains(network.acronym) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let network = networks[indexPath.row]
if let index = savedNetworks.firstIndex(of: network.acronym) {
savedNetworks.remove(at: index)
} else {
savedNetworks.append(network.acronym)
}
// reload all rows with the given acronym
let indexes = networks
.enumerated()
.filter { $0.element.acronym == network.acronym }
.map { IndexPath(row: $0.offset, section: 0) }
tableView.reloadRows(at: indexes, with: .automatic)
}
// MARK: - Actions
@IBAction func cancelTapped(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
@IBAction func saveTapped(_ sender: Any) {
// save selected networks
EQNUserData.shared.saveSelectedSeismicNetworks(savedNetworks)
// se solo un'ente è selezionato, salviamolo anche come nazione
if savedNetworks.count == 1 {
UserDefaults.standard.set(savedNetworks.first!, forKey: IMPOSTAZIONE_NAZIONE_RETI_SISMICHE)
} else {
UserDefaults.standard.removeObject(forKey: IMPOSTAZIONE_NAZIONE_RETI_SISMICHE)
}
delegate?.seismicSettingsNetworksControllerDidComplete(self)
dismiss(animated: true, completion: nil)
}
}
@@ -1,130 +0,0 @@
//
// SeismicSettingsViewController.swift
// Earthquake Network
//
// Created by Busi Andrea on 13/09/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
import Foundation
protocol SeismicSettingsViewControllerDelegate: AnyObject {
func seismicSettingsControllerDidComplete(_ controller: SeismicSettingsViewController)
func seismicSettingsControllerWillOpenProviders(_ controller: SeismicSettingsViewController)
}
class SeismicSettingsViewController: UIViewController {
weak var delegate: SeismicSettingsViewControllerDelegate?
// MARK: - Private
@IBOutlet private weak var containerView: UIView!
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var countryTextField: UITextField!
@IBOutlet private weak var confirmButton: UIButton!
@IBOutlet private weak var otherwiseLabel: UILabel!
@IBOutlet private weak var manageNetworksButton: UIButton!
@IBOutlet private weak var cancelButton: UIButton!
private let networks = EQNData.seismicNetworks().sorted(by: { $0.country < $1.country })
private let picker = EQNGenericPickerViewController()
private var selectedNetwork: EQNSeismicNetwork?
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
// MARK: - Private
private func setupUI() {
containerView.layer.cornerRadius = AppTheme.shared.cardCornerRadius
containerView.layer.masksToBounds = true
// localize
titleLabel.text = NSLocalizedString("official_select_country", comment: "")
countryTextField.placeholder = NSLocalizedString("official_select_country_placeholder", comment: "")
confirmButton.setLocalizedTitle(key: "official_select_confirm", uppercased: false)
otherwiseLabel.text = NSLocalizedString("official_select_or", comment: "")
manageNetworksButton.setLocalizedTitle(key: "official_select_networks", uppercased: false)
cancelButton.setLocalizedTitle(key: "status_cancel", uppercased: false)
// load saved country (if exists)
let savedCountry = UserDefaults.standard.object(forKey: IMPOSTAZIONE_NAZIONE_RETI_SISMICHE) as? String
selectedNetwork = EQNData.seismic(for: savedCountry)
countryTextField.text = selectedNetwork?.country
countryTextField.inputView = picker.view
let selectedIndex: Int? = selectedNetwork != nil ? networks.firstIndex(of: selectedNetwork!) : nil
picker.configure(with: networks, selectedIndex: selectedIndex) { [unowned self] (network) in
guard let network = network as? EQNSeismicNetwork else { return }
self.view.endEditing(true)
self.selectedNetwork = network
self.countryTextField.text = self.selectedNetwork?.country
}
picker.onCancel = { [unowned self] in
self.view.endEditing(true)
}
}
private func performSave(for network: EQNSeismicNetwork) {
// salviamo la sigla dell'ente selezionato
UserDefaults.standard.set(network.acronym, forKey: IMPOSTAZIONE_NAZIONE_RETI_SISMICHE)
// gli enti selezionati conterranno solo l'ente della nazione selezionata
let selectedNetworks = [network.acronym]
EQNUserData.shared.saveSelectedSeismicNetworks(selectedNetworks)
// aggiorniamo le impostazioni di notifica
EQNNotificheReteSismiche.shared().listaEnti = selectedNetworks
EQNNotificheReteSismiche.shared().saveUserInfo()
SettingsBaseViewController.saveSettings()
// informiamo il delegato
delegate?.seismicSettingsControllerDidComplete(self)
dismiss(animated: true, completion: nil)
}
// MARK: - Actions
@IBAction func confirmCountryTapped(_ sender: UIButton) {
guard let network = selectedNetwork else {
let alert = UIAlertController(title: NSLocalizedString("attention", comment: ""),
message: NSLocalizedString("official_no_country_selected", comment: ""),
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("ok", comment: ""), style: .cancel, handler: { [unowned self] (action) in
self.countryTextField.becomeFirstResponder()
}))
present(alert, animated: true, completion: nil)
return
}
// ask confirm to change settings for notifications
let alert = UIAlertController(title: NSLocalizedString("attention", comment: ""),
message: NSLocalizedString("official_select_message", comment: ""),
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("status_cancel", comment: ""), style: .cancel))
alert.addAction(UIAlertAction(title: NSLocalizedString("official_select_confirm", comment: ""), style: .default, handler: { [unowned self] (action) in
self.performSave(for: network)
}))
present(alert, animated: true, completion: nil)
}
@IBAction func selectNetworksTapped(_ sender: UIButton) {
delegate?.seismicSettingsControllerWillOpenProviders(self)
dismiss(animated: true, completion: nil)
}
@IBAction func cancelTapped(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
}
@@ -11,27 +11,27 @@ import Foundation
class SettingDateTableViewCell: UITableViewCell {
@objc static let Identifier = "DateCell"
static let Identifier = "DateCell"
@objc var isDisabled: Bool = false {
var isDisabled: Bool = false {
didSet {
updateUI()
}
}
@objc var isPickerVisible: Bool = false {
var isPickerVisible: Bool = false {
didSet {
if oldValue != isPickerVisible {
updateUI()
}
}
}
@objc private(set) var date = Date()
@objc var valueChanged: ((Date) -> Void)?
private(set) var date = Date()
var valueChanged: ((Date) -> Void)?
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -40,7 +40,7 @@ class SettingDateTableViewCell: UITableViewCell {
return label
}()
@objc lazy var valuesLabel: UILabel = {
lazy var valuesLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -58,7 +58,7 @@ class SettingDateTableViewCell: UITableViewCell {
return picker
}()
@objc lazy var stackView: UIStackView = {
lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
@@ -81,7 +81,7 @@ class SettingDateTableViewCell: UITableViewCell {
// MARK: - Public
@objc public func updateDate(_ date: Date) {
public func updateDate(_ date: Date) {
self.date = date
datePicker.setDate(date, animated: true)
}
@@ -10,17 +10,16 @@ import UIKit
class SettingDetailTableViewCell: UITableViewCell {
@objc static let Identifier = "DetailCell"
static let Identifier = "DetailCell"
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
@@ -10,10 +10,10 @@ import UIKit
class SettingEnableTableViewCell: UITableViewCell {
@objc static let Identifier = "EnableCell"
static let Identifier = "EnableCell"
@objc var valueChanged: ((Bool) -> Void)?
@objc var isDisabled: Bool = false {
var valueChanged: ((Bool) -> Void)?
var isDisabled: Bool = false {
didSet {
updateUI()
}
@@ -21,7 +21,7 @@ class SettingEnableTableViewCell: UITableViewCell {
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -29,7 +29,7 @@ class SettingEnableTableViewCell: UITableViewCell {
return label
}()
@objc lazy var descriptionLabel: UILabel = {
lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -37,7 +37,7 @@ class SettingEnableTableViewCell: UITableViewCell {
return label
}()
@objc lazy var toggleSwitch: UISwitch = {
lazy var toggleSwitch: UISwitch = {
let toggle = UISwitch()
toggle.setContentHuggingPriority(.required, for: .horizontal)
toggle.setContentCompressionResistancePriority(.required, for: .horizontal)
@@ -45,6 +45,15 @@ class SettingEnableTableViewCell: UITableViewCell {
return toggle
}()
lazy var errorLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = UIFont.preferredFont(forTextStyle: .subheadline)
label.textColor = AppTheme.Colors.red
label.text = nil
return label
}()
// MARK: - Init
@@ -75,6 +84,7 @@ class SettingEnableTableViewCell: UITableViewCell {
contentView.addSubview(stackView)
contentView.addSubview(descriptionLabel)
contentView.addSubview(errorLabel)
stackView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor).isActive = true
@@ -83,7 +93,12 @@ class SettingEnableTableViewCell: UITableViewCell {
descriptionLabel.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 8).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
descriptionLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
//descriptionLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
errorLabel.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 8.0).isActive = true
errorLabel.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
errorLabel.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor).isActive = true
errorLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
}
private func updateUI() {
@@ -11,9 +11,9 @@ import Foundation
class SettingMultivaluesTableViewCell: UITableViewCell {
@objc static let Identifier = "MultivaluesCell"
static let Identifier = "MultivaluesCell"
@objc var isDisabled: Bool = false {
var isDisabled: Bool = false {
didSet {
updateUI()
}
@@ -21,7 +21,7 @@ class SettingMultivaluesTableViewCell: UITableViewCell {
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -30,7 +30,7 @@ class SettingMultivaluesTableViewCell: UITableViewCell {
return label
}()
@objc lazy var valuesLabel: UILabel = {
lazy var valuesLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -10,18 +10,17 @@ import UIKit
class SettingSectionHeaderView: UITableViewHeaderFooterView {
@objc static let Identifier = "SectionHeaderView"
@objc static let Height = 50.0
static let Identifier = "SectionHeaderView"
static let Height = 50.0
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let titleLabel = UILabel()
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
titleLabel.textColor = AppTheme.Colors.lightBlue
return titleLabel
}()
// MARK: - Init
@@ -34,7 +33,6 @@ class SettingSectionHeaderView: UITableViewHeaderFooterView {
super.init(coder: coder)
setupUI()
}
// MARK: - Private
@@ -45,6 +43,5 @@ class SettingSectionHeaderView: UITableViewHeaderFooterView {
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
}
}
@@ -11,19 +11,19 @@ import Foundation
class SettingSegmentedTableViewCell: UITableViewCell {
@objc static let Identifier = "SegmentedCell"
static let Identifier = "SegmentedCell"
@objc var isDisabled: Bool = false {
var isDisabled: Bool = false {
didSet {
updateUI()
}
}
@objc var valueChanged: ((EQNGenericValue) -> Void)?
var valueChanged: ((EQNGenericValue) -> Void)?
private var items = [EQNGenericValue]()
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -32,7 +32,7 @@ class SettingSegmentedTableViewCell: UITableViewCell {
return label
}()
@objc lazy var segmentedControl: UISegmentedControl = {
lazy var segmentedControl: UISegmentedControl = {
let segmented = UISegmentedControl()
segmented.translatesAutoresizingMaskIntoConstraints = false
segmented.addTarget(self, action: #selector(segmentedControlChanged(_:)), for: .valueChanged)
@@ -10,21 +10,21 @@ import UIKit
class SettingSliderTableViewCell: UITableViewCell {
@objc static let Identifier = "SliderCell"
static let Identifier = "SliderCell"
@objc var isDisabled: Bool = false {
var isDisabled: Bool = false {
didSet {
updateUI()
}
}
@objc var valueChanged: ((EQNGenericValue) -> Void)?
@objc var dragEnded: (() -> Void)?
var valueChanged: ((EQNGenericValue) -> Void)?
var dragEnded: (() -> Void)?
private var items = [EQNGenericValue]()
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -33,7 +33,7 @@ class SettingSliderTableViewCell: UITableViewCell {
return label
}()
@objc lazy var valueLabel: UILabel = {
lazy var valueLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -42,7 +42,7 @@ class SettingSliderTableViewCell: UITableViewCell {
return label
}()
@objc lazy var slider: UISlider = {
lazy var slider: UISlider = {
let slider = UISlider()
slider.isContinuous = true
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
@@ -0,0 +1,43 @@
//
// SettingsBaseTableViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 10/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import UIKit
@objc
class SettingsBaseTableViewController: UITableViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParent {
Self.saveSettings()
}
}
// MARK: - Class
@objc class func saveSettings() {
saveSettings { _ in
// nope
}
}
@objc class func saveSettings(
completion: @escaping (_ success: Bool) -> Void
) {
let url = EQNGeneratoreURLServer.urlInvioImpostazioniNotifiche()
ServerRequest.default().inviaInformazioniAlServer(with: url, richiesta: .impostazioniNotifiche) { _ in
print("[SETTINGS] Settings saved successfully")
completion(true)
} failure: { error in
print("[SETTINGS] Settings saved failed. Error: \(error?.localizedDescription ?? "n.d.")")
completion(false)
}
}
}
@@ -1,19 +0,0 @@
//
// SettingsBaseViewController.h
// Earthquake Network
//
// Created by Busi Andrea on 30/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface SettingsBaseViewController : UITableViewController
+ (void)saveSettings;
@end
NS_ASSUME_NONNULL_END
@@ -1,42 +0,0 @@
//
// SettingsBaseViewController.m
// Earthquake Network
//
// Created by Busi Andrea on 30/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "SettingsBaseViewController.h"
#import "ServerRequest.h"
#import "EQNGeneratoreURLServer.h"
@interface SettingsBaseViewController ()
@end
@implementation SettingsBaseViewController
#pragma mark - View Lifecycle
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// when controller is dismissed, save settings
if (self.isMovingFromParentViewController) {
[SettingsBaseViewController saveSettings];
}
}
#pragma mark - Private
+ (void)saveSettings
{
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:[EQNGeneratoreURLServer urlInvioImpostazioniNotifiche] richiesta:EQNTipoChiamataImpostazioniNotifiche success:^(id result){
NSLog(@"Settings saved successfully");
} failure:^(NSError *error){
NSLog(@"Settings saved failed. Error: %@", error.localizedDescription);
}];
}
@end
@@ -1,18 +0,0 @@
//
// AletaSismiTableViewController.h
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "SettingsBaseViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface SettingsRealTimeAlertsViewController : SettingsBaseViewController
@end
NS_ASSUME_NONNULL_END
@@ -1,165 +0,0 @@
//
// AletaSismiTableViewController.m
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "SettingsRealTimeAlertsViewController.h"
#import "EQNAllertaSismica.h"
@import UserNotifications;
@interface SettingsRealTimeAlertsViewController () <UITextFieldDelegate>
@property (nonatomic, strong) NSArray<SettingItem *> *settings;
@property (nonatomic, strong) NSDateFormatter *dateFormatter;
@property (nonatomic, assign) BOOL notificationEnabled;
@property (nonatomic, assign) BOOL criticalAlertsEnabled;
@end
@implementation SettingsRealTimeAlertsViewController
typedef NS_ENUM(NSInteger, RowIdentifier) {
RowIdentifierAbilitaNotifiche = 0,
RowIdentifierAbilitaCriticalAlerts
};
#pragma mark - Accessories
- (NSDateFormatter *)dateFormatter
{
if (!_dateFormatter) {
_dateFormatter = [[NSDateFormatter alloc] init];
[_dateFormatter setDateFormat:@"HH:mm"];
}
return _dateFormatter;
}
#pragma mark - View Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self setupUI];
self.settings = @[
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"options_notification_enable_alarm", @"")],
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"critical_alerts_setting", @"")]
];
[self loadDataSource];
[self.tableView reloadData];
}
#pragma mark - Private
- (void)setupUI
{
self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever;
self.tableView.estimatedRowHeight = 200.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.tableView registerClass:[SettingSectionHeaderView class] forHeaderFooterViewReuseIdentifier:SettingSectionHeaderView.Identifier];
[self.tableView registerClass:[SettingEnableTableViewCell class] forCellReuseIdentifier:SettingEnableTableViewCell.Identifier];
[self.tableView registerClass:[SettingSliderTableViewCell class] forCellReuseIdentifier:SettingSliderTableViewCell.Identifier];
[self.tableView registerClass:[SettingMultivaluesTableViewCell class] forCellReuseIdentifier:SettingMultivaluesTableViewCell.Identifier];
[self.tableView registerClass:[SettingSegmentedTableViewCell class] forCellReuseIdentifier:SettingSegmentedTableViewCell.Identifier];
[self.tableView registerClass:[SettingDateTableViewCell class] forCellReuseIdentifier:SettingDateTableViewCell.Identifier];
}
- (void)loadDataSource
{
self.notificationEnabled = [EQNAllertaSismica sharedInstance].isAbilitato;
self.criticalAlertsEnabled = [EQNAllertaSismica sharedInstance].isCriticalAlertsEnabled;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.settings.count;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
SettingSectionHeaderView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:SettingSectionHeaderView.Identifier];
headerView.titleLabel.text = NSLocalizedString(@"options_alarms", @"");
return headerView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return SettingSectionHeaderView.Height;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
SettingItem *setting = self.settings[indexPath.row];
if (setting.type == SettingTypeEnable) {
SettingEnableTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingEnableTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
cell.descriptionLabel.text = setting.subtitle;
if (indexPath.row == RowIdentifierAbilitaNotifiche) {
cell.toggleSwitch.on = self.notificationEnabled;
cell.valueChanged = ^(BOOL enabled) {
self.notificationEnabled = enabled;
[EQNAllertaSismica sharedInstance].isAbilitato = self.notificationEnabled;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
} else if (indexPath.row == RowIdentifierAbilitaCriticalAlerts) {
cell.toggleSwitch.on = self.criticalAlertsEnabled;
cell.valueChanged = ^(BOOL enabled) {
if (enabled) {
[self askForCriticalAlertsPermission];
}
self.criticalAlertsEnabled = enabled;
[EQNAllertaSismica sharedInstance].isCriticalAlertsEnabled = self.criticalAlertsEnabled;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
}
return cell;
} else if (setting.type == SettingTypeSegmented) {
SettingSegmentedTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingSegmentedTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
return cell;
} else if (setting.type == SettingTypeSlider) {
SettingSliderTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingSliderTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
return cell;
}
return nil;
}
#pragma mark - Private
- (void)updateSismicToNotify:(EQNGenericValue *)seismic
{
[EQNAllertaSismica sharedInstance].sismiDaNotificare = seismic.value;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self loadDataSource];
}
- (void)askForCriticalAlertsPermission
{
UNAuthorizationOptions authOptions = UNAuthorizationOptionCriticalAlert;
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError *error) {
// nope
}];
}
@end
@@ -0,0 +1,136 @@
//
// SettingsRealTimeAlertsViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 10/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import UIKit
import Shogun
class SettingsRealTimeAlertsViewController: SettingsBaseTableViewController {
private enum RowIdentifier: Int {
case abilitaNotifiche
case abilitaCriticalAlerts
}
private var isNotificationEnabled = false
private var isCriticalAlertsEnabled = false
private let settings: [SettingItem] = [
.init(type: .enable, title: NSLocalizedString("options_notification_enable_alarm", comment: "")),
.init(type: .enable, title: NSLocalizedString("critical_alerts_setting", comment: ""))
]
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
tableView.reloadData()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadDataSource()
tableView.reloadData()
}
// MARK: - Private
private func setupUI() {
navigationItem.largeTitleDisplayMode = .never
tableView.estimatedRowHeight = 200.0
tableView.rowHeight = UITableView.automaticDimension
tableView.registerHeaderFooterView(for: SettingSectionHeaderView.self)
tableView.registerCell(for: SettingEnableTableViewCell.self)
}
private func loadDataSource() {
let saved = EQNSettingRealTimeAlert.shared
isNotificationEnabled = saved.isAbilitato
isCriticalAlertsEnabled = saved.isCriticalAlertsEnabled
}
// MARK: - Table view delegate and data source
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueHeaderFooterView(cellIdentifiable: SettingSectionHeaderView.self)
view.titleLabel.text = NSLocalizedString("options_alarms", comment: "")
return view
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
SettingSectionHeaderView.Height
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return settings.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let identifier = RowIdentifier(rawValue: indexPath.row) else {
return UITableViewCell()
}
let setting = settings[indexPath.row]
switch setting.type {
case .enable:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SettingEnableTableViewCell.self, for: indexPath)
cell.titleLabel.text = setting.displayTitle
cell.descriptionLabel.text = setting.subtitle
switch identifier {
case .abilitaNotifiche:
cell.toggleSwitch.isOn = isNotificationEnabled
cell.valueChanged = { [weak self] enabled in
self?.onChangeNotificationEnabled(enabled)
}
case .abilitaCriticalAlerts:
cell.toggleSwitch.isOn = isCriticalAlertsEnabled
cell.valueChanged = { [weak self] enabled in
self?.onChangeCriticalAlertsEnabled(enabled)
}
}
return cell
default:
fatalError()
}
}
// MARK: - Private
private func onChangeNotificationEnabled(_ enabled: Bool) {
isNotificationEnabled = enabled
EQNSettingRealTimeAlert.shared.isAbilitato = isNotificationEnabled
EQNSettingRealTimeAlert.shared.saveUserInfo()
tableView.reloadData()
}
private func onChangeCriticalAlertsEnabled(_ enabled: Bool) {
if enabled {
askForCriticalAlertsPermission()
}
isCriticalAlertsEnabled = enabled
EQNSettingRealTimeAlert.shared.isCriticalAlertsEnabled = isCriticalAlertsEnabled
EQNSettingRealTimeAlert.shared.saveUserInfo()
tableView.reloadData()
}
private func askForCriticalAlertsPermission() {
UNUserNotificationCenter.current().requestAuthorization(options: [ .criticalAlert ]) { granted, error in
// nope
}
}
}
@@ -1,18 +0,0 @@
//
// SettingsSeismicNetworkAlertsViewController.h
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "SettingsBaseViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface SettingsSeismicNetworkAlertsViewController : SettingsBaseViewController
@end
NS_ASSUME_NONNULL_END
@@ -1,258 +0,0 @@
//
// SettingsSeismicNetworkAlertsViewController.m
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "SettingsSeismicNetworkAlertsViewController.h"
#import "EQNNotificheReteSismiche.h"
@interface SettingsSeismicNetworkAlertsViewController ()
@property (nonatomic, strong) NSArray<SettingItem *> *settings;
@property (nonatomic, strong) NSArray<EQNGenericValue *> *dataSourceRaggioSisma;
@property (nonatomic, strong) NSArray<EQNGenericValue *> *dataSourceMagnitudoDeboli;
@property (nonatomic, strong) NSArray<EQNGenericValue *> *dataSourceMagnitudoForti;
@property (nonatomic, assign) BOOL notificationEnabled;
@property (nonatomic, assign) BOOL notificationNearEarthquakeEnabled;
@property (nonatomic, assign) BOOL notificationStrongEarthquakeEnabled;
@property (nonatomic, strong) EQNGenericValue *currentUserPositionRadius;
@property (nonatomic, strong) EQNGenericValue *currentSeismicEnergy;
@property (nonatomic, strong) EQNGenericValue *currentStrongEarthquakeDistance;
@end
@implementation SettingsSeismicNetworkAlertsViewController
static NSString * const SegueIdentifierListaEnti = @"ShowListaEnti";
typedef NS_ENUM(NSInteger, RowIdentifier) {
RowIdentifierAbilitaNotifiche = 0,
RowIdentifierRetiSismiche,
RowIdentifierRaggioPosizione,
RowIdentifierEnergiaSisma,
RowIdentifierTerremotiVicini,
RowIdentifierTerremotiForti,
RowIdentifierTerremotiFortiDistanza
};
#pragma mark - View Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self setupUI];
self.settings = @[
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"options_notification_enable_official", @"")],
[[SettingItem alloc] initWithType:SettingTypeMultiValues title:NSLocalizedString(@"options_agencies", @"") segue:SegueIdentifierListaEnti],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_radius", @"")],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_energy", @"")],
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"options_near", @"") subtitle:NSLocalizedString(@"options_near_alert", @"")],
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"options_strong", @"") subtitle:NSLocalizedString(@"options_strong_alert", @"")],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_strong_magnitude", @"")]
];
self.dataSourceMagnitudoDeboli = [EQNData magitudoDeboli];
self.dataSourceRaggioSisma = [EQNData raggioSismi];
self.dataSourceMagnitudoForti = [EQNData magitudoForti];
[self loadDataSource];
[self.tableView reloadData];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self loadDataSource];
[self.tableView reloadData];
}
#pragma mark - Private
- (void)setupUI
{
self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever;
self.tableView.estimatedRowHeight = 200.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.tableView registerClass:[SettingSectionHeaderView class] forHeaderFooterViewReuseIdentifier:SettingSectionHeaderView.Identifier];
[self.tableView registerClass:[SettingEnableTableViewCell class] forCellReuseIdentifier:SettingEnableTableViewCell.Identifier];
[self.tableView registerClass:[SettingSliderTableViewCell class] forCellReuseIdentifier:SettingSliderTableViewCell.Identifier];
[self.tableView registerClass:[SettingMultivaluesTableViewCell class] forCellReuseIdentifier:SettingMultivaluesTableViewCell.Identifier];
}
- (void)loadDataSource
{
self.notificationEnabled = [EQNNotificheReteSismiche sharedInstance].isAbilitato;
self.notificationNearEarthquakeEnabled = [EQNNotificheReteSismiche sharedInstance].isAbilitaVicini;
self.notificationStrongEarthquakeEnabled = [EQNNotificheReteSismiche sharedInstance].isTerremortiForti;
// raggio dalla tua posizione
EQNGenericValue *raggioSisma = [EQNData raggioSismaFor:[EQNNotificheReteSismiche sharedInstance].distanzaPosizione];
self.currentUserPositionRadius = raggioSisma;
// energia sisma
EQNGenericValue *energiaSisma = [EQNData magitudoDeboleFor:[EQNNotificheReteSismiche sharedInstance].energiaSisma];
self.currentSeismicEnergy = energiaSisma;
// terremoti forti
EQNGenericValue *terremotiForti = [EQNData magitudoForteFor:[EQNNotificheReteSismiche sharedInstance].energiaTerremotiForti];
self.currentStrongEarthquakeDistance = terremotiForti;
// enti
if (![EQNNotificheReteSismiche sharedInstance].listaEnti) {
[EQNNotificheReteSismiche sharedInstance].listaEnti = [EQNData.seismicNetworkAcronyms copy];
}
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
}
#pragma mark - Table view data source
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
SettingSectionHeaderView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:SettingSectionHeaderView.Identifier];
headerView.titleLabel.text = NSLocalizedString(@"options_notification_official", @"titolo impostazioni notifiche");
return headerView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return SettingSectionHeaderView.Height;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.settings.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
SettingItem *setting = self.settings[indexPath.row];
if (setting.type == SettingTypeEnable) {
SettingEnableTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingEnableTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
cell.descriptionLabel.text = setting.subtitle;
if (indexPath.row == RowIdentifierAbilitaNotifiche) {
cell.toggleSwitch.on = self.notificationEnabled;
cell.valueChanged = ^(BOOL enabled) {
self.notificationEnabled = enabled;
[EQNNotificheReteSismiche sharedInstance].isAbilitato = self.notificationEnabled;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
} else if (indexPath.row == RowIdentifierTerremotiVicini) {
cell.toggleSwitch.on = self.notificationNearEarthquakeEnabled;
cell.isDisabled = !self.notificationEnabled;
cell.valueChanged = ^(BOOL enabled) {
self.notificationNearEarthquakeEnabled = enabled;
[EQNNotificheReteSismiche sharedInstance].isAbilitaVicini = self.notificationNearEarthquakeEnabled;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
} else if (indexPath.row == RowIdentifierTerremotiForti) {
cell.toggleSwitch.on = self.notificationStrongEarthquakeEnabled;
cell.isDisabled = !self.notificationEnabled;
cell.valueChanged = ^(BOOL enabled) {
self.notificationStrongEarthquakeEnabled = enabled;
[EQNNotificheReteSismiche sharedInstance].isTerremortiForti = self.notificationStrongEarthquakeEnabled;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
}
return cell;
} else if (setting.type == SettingTypeMultiValues) {
SettingMultivaluesTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingMultivaluesTableViewCell.Identifier forIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.isDisabled = !self.notificationEnabled;
cell.userInteractionEnabled = self.notificationEnabled;
cell.titleLabel.text = setting.title;
if (indexPath.row == RowIdentifierRetiSismiche) {
cell.valuesLabel.text = [self stringOfSelectedNetworks];
}
return cell;
} else if (setting.type == SettingTypeSlider) {
SettingSliderTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingSliderTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
if (indexPath.row == RowIdentifierRaggioPosizione) {
cell.isDisabled = !self.notificationEnabled;
[cell configureSliderWith:self.dataSourceRaggioSisma current:self.currentUserPositionRadius];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateUserPositionRadius:item];
};
} else if (indexPath.row == RowIdentifierEnergiaSisma) {
cell.isDisabled = !self.notificationEnabled;
[cell configureSliderWith:self.dataSourceMagnitudoDeboli current:self.currentSeismicEnergy];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateSeismicEnergy:item];
};
} else if (indexPath.row == RowIdentifierTerremotiFortiDistanza) {
cell.isDisabled = !self.notificationEnabled || !self.notificationStrongEarthquakeEnabled;
[cell configureSliderWith:self.dataSourceMagnitudoForti current:self.currentStrongEarthquakeDistance];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateStrongEarthquakeEnergy:item];
};
}
return cell;
}
return nil;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
SettingItem *setting = self.settings[indexPath.row];
if (setting.segue != nil) {
[self performSegueWithIdentifier:setting.segue sender:nil];
}
}
#pragma mark - Private
- (void)updateUserPositionRadius:(EQNGenericValue *)radius
{
[EQNNotificheReteSismiche sharedInstance].distanzaPosizione = radius.value;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self loadDataSource];
}
- (void)updateSeismicEnergy:(EQNGenericValue *)energy
{
[EQNNotificheReteSismiche sharedInstance].energiaSisma = energy.value;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self loadDataSource];
}
- (void)updateStrongEarthquakeEnergy:(EQNGenericValue *)energy
{
[EQNNotificheReteSismiche sharedInstance].energiaTerremotiForti = energy.value;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self loadDataSource];
}
- (NSString *)stringOfSelectedNetworks
{
NSArray *networks = [EQNNotificheReteSismiche sharedInstance].listaEnti;
networks = [networks sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
return [networks componentsJoinedByString:@", "];
}
@end
@@ -0,0 +1,166 @@
//
// SettingsSeismicNetworkNotificationsViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 06/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import UIKit
import Shogun
class SettingsSeismicNetworkNotificationsViewController: SettingsBaseTableViewController {
private enum RowIdentifier: Int {
case abilitaNotifiche
case magnitudoMinima
case distanzaMassima
}
private var isNotificationEnabled = false
private var currentMinimumMagnitude = EQNData.DefaultSettingSeismicNetworkNotificationMagitude
private var currentMaximumDistance = EQNData.DefaultSettingSeismicNetworkNotificationRadius
private let dataSourceMinimumMagnitude = EQNData.settingSeismicNetworkNotificationMagnitudes
private let dataSourceMaximumDistance = EQNData.settingSeismicNetworkNotificationRadius
private let settings: [SettingItem] = [
.init(type: .enable, title: NSLocalizedString("options_notification_enable_official", comment: "")),
.init(type: .slider, title: NSLocalizedString("options_official_minmag", comment: "")),
.init(type: .slider, title: NSLocalizedString("options_official_maxdist", comment: ""))
]
// MARK: - View Liefcycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
tableView.reloadData()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadDataSource()
tableView.reloadData()
}
// MARK: - Private
private func setupUI() {
navigationItem.largeTitleDisplayMode = .never
tableView.estimatedRowHeight = 200.0
tableView.rowHeight = UITableView.automaticDimension
tableView.registerHeaderFooterView(for: SettingSectionHeaderView.self)
tableView.registerCell(for: SettingEnableTableViewCell.self)
tableView.registerCell(for: SettingSliderTableViewCell.self)
tableView.registerCell(for: SettingMultivaluesTableViewCell.self)
}
private func loadDataSource() {
let saved = EQNSettingSeismicNetworkNotification.shared
isNotificationEnabled = saved.isAbilitato
// magnitudo minima
let magnitudoMinima = EQNData.getSettingSeismicNetworkNotificationMagnitudes(for: saved.magnitudoMinima)
currentMinimumMagnitude = magnitudoMinima
// raggio dalla tua posizione
let distanzaMassima = EQNData.getSettingSeismicNetworkNotificationRadius(for: saved.distanzaMassima)
currentMaximumDistance = distanzaMassima
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueHeaderFooterView(cellIdentifiable: SettingSectionHeaderView.self)
view.titleLabel.text = NSLocalizedString("options_notification_official", comment: "")
return view
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
SettingSectionHeaderView.Height
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return settings.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let identifier = RowIdentifier(rawValue: indexPath.row) else {
return UITableViewCell()
}
let setting = settings[indexPath.row]
switch setting.type {
case .enable:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SettingEnableTableViewCell.self, for: indexPath)
cell.titleLabel.text = setting.displayTitle
cell.descriptionLabel.text = setting.subtitle
switch identifier {
case .abilitaNotifiche:
cell.toggleSwitch.isOn = isNotificationEnabled
cell.valueChanged = { [weak self] enabled in
self?.onChangeNotificationEnabled(enabled)
}
default:
break
}
return cell
case .slider:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SettingSliderTableViewCell.self, for: indexPath)
cell.titleLabel.text = setting.displayTitle
let filtersEnabled = isNotificationEnabled
switch identifier {
case .magnitudoMinima:
cell.isDisabled = !filtersEnabled
cell.configureSlider(with: dataSourceMinimumMagnitude, current: currentMinimumMagnitude)
cell.valueChanged = { [weak self] item in
self?.onChangeMinimumMagnitude(item)
}
case .distanzaMassima:
cell.isDisabled = !filtersEnabled
cell.configureSlider(with: dataSourceMaximumDistance, current: currentMaximumDistance)
cell.valueChanged = { [weak self] item in
self?.onChangeMaximumDistance(item)
}
default:
break
}
return cell
default:
fatalError()
}
}
private func onChangeNotificationEnabled(_ enabled: Bool) {
isNotificationEnabled = enabled
EQNSettingSeismicNetworkNotification.shared.isAbilitato = isNotificationEnabled
EQNSettingSeismicNetworkNotification.shared.saveUserInfo()
tableView.reloadData()
}
private func onChangeMinimumMagnitude(_ item: EQNGenericValue) {
EQNSettingSeismicNetworkNotification.shared.magnitudoMinima = item.value
EQNSettingSeismicNetworkNotification.shared.saveUserInfo()
loadDataSource()
}
private func onChangeMaximumDistance(_ item: EQNGenericValue) {
EQNSettingSeismicNetworkNotification.shared.distanzaMassima = item.value
EQNSettingSeismicNetworkNotification.shared.saveUserInfo()
loadDataSource()
}
}
@@ -1,71 +0,0 @@
//
// SettingsSeismicNetworksViewController.swift
// Earthquake Network
//
// Created by Busi Andrea on 26/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
import UIKit
class SettingsSeismicNetworksViewController: UITableViewController {
var networks = [EQNSeismicNetwork]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(SettingDetailTableViewCell.self, forCellReuseIdentifier: SettingDetailTableViewCell.Identifier)
tableView.register(SettingSectionHeaderView.self, forHeaderFooterViewReuseIdentifier: SettingSectionHeaderView.Identifier)
networks = EQNData.seismicNetworks().sorted(by: { $0.acronym < $1.acronym })
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SettingSectionHeaderView.Identifier) as! SettingSectionHeaderView
headerView.titleLabel.text = NSLocalizedString("options_agencies", comment: "");
return headerView
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
CGFloat(SettingSectionHeaderView.Height)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
networks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let network = networks[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: SettingDetailTableViewCell.Identifier, for: indexPath) as! SettingDetailTableViewCell
cell.textLabel?.text = "\(network.acronym) (\(network.country))"
if EQNNotificheReteSismiche.shared().listaEnti.contains(network.acronym) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let network = networks[indexPath.row]
var savedNetworks = EQNNotificheReteSismiche.shared().listaEnti
if let index = savedNetworks.firstIndex(where: { $0 == network.acronym }) {
savedNetworks.remove(at: index)
} else {
savedNetworks.append(network.acronym)
}
EQNNotificheReteSismiche.shared().listaEnti = savedNetworks
EQNNotificheReteSismiche.shared().saveUserInfo()
tableView.reloadData()
}
}
@@ -1,18 +0,0 @@
//
// SettingsUserReportAlertsViewController.h
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "SettingsBaseViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface SettingsUserReportAlertsViewController : SettingsBaseViewController
@end
NS_ASSUME_NONNULL_END
@@ -1,119 +0,0 @@
//
// SettingsUserReportAlertsViewController.m
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "SettingsUserReportAlertsViewController.h"
#import "EQNNotificheSegnalazioniUtente.h"
@interface SettingsUserReportAlertsViewController ()
@property (nonatomic, strong) NSArray<SettingItem *> *settings;
@property (nonatomic, assign) BOOL notificationsEnabled;
@property (nonatomic, strong) EQNGenericValue *currentRadius;
@end
@implementation SettingsUserReportAlertsViewController
#pragma mark - View Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self setupUI];
self.settings = @[
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"options_notification_enable_manual", @"")],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_radius", @"")]
];
[self updateUI];
}
#pragma mark - Private
- (void)setupUI
{
self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever;
self.tableView.estimatedRowHeight = 100.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.tableView registerClass:[SettingSectionHeaderView class] forHeaderFooterViewReuseIdentifier:SettingSectionHeaderView.Identifier];
[self.tableView registerClass:[SettingEnableTableViewCell class] forCellReuseIdentifier:SettingEnableTableViewCell.Identifier];
[self.tableView registerClass:[SettingSliderTableViewCell class] forCellReuseIdentifier:SettingSliderTableViewCell.Identifier];
}
- (void)updateUI
{
self.notificationsEnabled = [EQNNotificheSegnalazioniUtente sharedInstance].isAbilitato;
EQNGenericValue *distanzaPosizione = [EQNData raggioSismaFor:[EQNNotificheSegnalazioniUtente sharedInstance].distanzaPosizione];
self.currentRadius = distanzaPosizione;
[[EQNNotificheSegnalazioniUtente sharedInstance] saveUserInfo];
[self.tableView reloadData];
}
- (void)updateRadius:(EQNGenericValue *)radius
{
self.currentRadius = radius;
[EQNNotificheSegnalazioniUtente sharedInstance].distanzaPosizione = radius.value;
[[EQNNotificheSegnalazioniUtente sharedInstance] saveUserInfo];
[self.tableView reloadData];
}
#pragma mark - Table view data source
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
SettingSectionHeaderView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:SettingSectionHeaderView.Identifier];
headerView.titleLabel.text = NSLocalizedString(@"options_notification_manual", @"titolo impostazioni notifiche");
return headerView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return SettingSectionHeaderView.Height;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.settings.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
SettingItem *setting = self.settings[indexPath.row];
if (setting.type == SettingTypeEnable) {
SettingEnableTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingEnableTableViewCell.Identifier forIndexPath:indexPath];
cell.toggleSwitch.on = self.notificationsEnabled;
cell.titleLabel.text = setting.displayTitle;
cell.descriptionLabel.text = setting.subtitle;
cell.valueChanged = ^(BOOL enabled) {
self.notificationsEnabled = enabled;
[EQNNotificheSegnalazioniUtente sharedInstance].isAbilitato = self.notificationsEnabled;
[[EQNNotificheSegnalazioniUtente sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
return cell;
} else if (setting.type == SettingTypeSlider) {
SettingSliderTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingSliderTableViewCell.Identifier forIndexPath:indexPath];
cell.isDisabled = !self.notificationsEnabled;
cell.titleLabel.text = setting.displayTitle;
[cell configureSliderWith:[EQNData raggioSismi] current:self.currentRadius];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateRadius:item];
};
return cell;
}
return nil;
}
@end
@@ -0,0 +1,145 @@
//
// SettingsUserReportNotificationsViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 10/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import UIKit
import Shogun
class SettingsUserReportNotificationsViewController: SettingsBaseTableViewController {
private enum RowIdentifier: Int {
case abilitaNotifiche
case distanzaMassima
}
private var isNotificationEnabled = false
private var currentMaximumDistance = EQNData.DefaultSettingUserReportNotificationRadius
private let dataSourceMaximumDistance = EQNData.settingUserReportNotificationRadius
private let settings: [SettingItem] = [
.init(type: .enable, title: NSLocalizedString("options_notification_enable_manual", comment: "")),
.init(type: .slider, title: NSLocalizedString("options_radius", comment: ""))
]
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
tableView.reloadData()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadDataSource()
tableView.reloadData()
}
// MARK: - Private
private func setupUI() {
navigationItem.largeTitleDisplayMode = .never
tableView.estimatedRowHeight = 200.0
tableView.rowHeight = UITableView.automaticDimension
tableView.registerHeaderFooterView(for: SettingSectionHeaderView.self)
tableView.registerCell(for: SettingEnableTableViewCell.self)
tableView.registerCell(for: SettingSliderTableViewCell.self)
}
private func loadDataSource() {
let saved = EQNSettingUserReportNotification.shared
isNotificationEnabled = saved.isAbilitato
let distanzaMassima = EQNData.getSettingUserReportNotificationRadius(for: saved.distanzaMassima)
currentMaximumDistance = distanzaMassima
}
// MARK: - Table view delegate and data source
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueHeaderFooterView(cellIdentifiable: SettingSectionHeaderView.self)
view.titleLabel.text = NSLocalizedString("options_notification_manual", comment: "")
return view
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
SettingSectionHeaderView.Height
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return settings.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let identifier = RowIdentifier(rawValue: indexPath.row) else {
return UITableViewCell()
}
let setting = settings[indexPath.row]
switch setting.type {
case .enable:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SettingEnableTableViewCell.self, for: indexPath)
cell.titleLabel.text = setting.displayTitle
cell.descriptionLabel.text = setting.subtitle
switch identifier {
case .abilitaNotifiche:
cell.toggleSwitch.isOn = isNotificationEnabled
cell.valueChanged = { [weak self] enabled in
self?.onChangeNotificationEnabled(enabled)
}
default:
break
}
return cell
case .slider:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SettingSliderTableViewCell.self, for: indexPath)
cell.titleLabel.text = setting.displayTitle
switch identifier {
case .distanzaMassima:
cell.isDisabled = !isNotificationEnabled
cell.configureSlider(with: dataSourceMaximumDistance, current: currentMaximumDistance)
cell.valueChanged = { [weak self] item in
self?.onChangeMaximumDistance(item)
}
default:
break
}
return cell
default:
fatalError()
}
}
// MARK: - Private
private func onChangeNotificationEnabled(_ enabled: Bool) {
isNotificationEnabled = enabled
EQNSettingUserReportNotification.shared.isAbilitato = isNotificationEnabled
EQNSettingUserReportNotification.shared.saveUserInfo()
tableView.reloadData()
}
private func onChangeMaximumDistance(_ item: EQNGenericValue) {
EQNSettingUserReportNotification.shared.distanzaMassima = item.value
EQNSettingUserReportNotification.shared.saveUserInfo()
loadDataSource()
}
}
@@ -35,7 +35,7 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
// MARK: - Internal
/// Annotations displayed on the map
private var mapAnnotations = [MKAnnotation]()
private(set) var mapAnnotations = [MKAnnotation]()
/// If `true`, the initial filter has been already evaluated
private var initialFilterEvaluated = false
@@ -104,6 +104,34 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
return label
}()
// app icon and name displayed on the screenshot
private lazy var watermarkView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
let logo = UIImageView(image: .init(named: "eq_icon_transparent"))
logo.translatesAutoresizingMaskIntoConstraints = false
logo.contentMode = .scaleAspectFit
view.addSubview(logo)
logo.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
logo.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
logo.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
logo.widthAnchor.constraint(equalTo: logo.heightAnchor).isActive = true
let title = UILabel()
title.translatesAutoresizingMaskIntoConstraints = false
title.text = NSLocalizedString("app_name", comment: "") + " App"
title.textColor = AppTheme.Colors.red
title.font = .preferredFont(forTextStyle: .title3, weight: .semibold)
view.addSubview(title)
title.leadingAnchor.constraint(equalTo: logo.trailingAnchor, constant: 10.0).isActive = true
title.centerYAnchor.constraint(equalTo: logo.centerYAnchor).isActive = true
title.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
return view
}()
// MARK: - Init
init() {
@@ -155,6 +183,18 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
extraUI()
}
private func addWatermarkView() {
view.addSubview(watermarkView)
watermarkView.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 10.0).isActive = true
watermarkView.leadingAnchor.constraint(equalTo: mapView.leadingAnchor, constant: 10.0).isActive = true
watermarkView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
}
private func removeWatermarkView() {
watermarkView.removeFromSuperview()
}
// MARK: - View Lifecycle
override func viewDidLoad() {
@@ -238,6 +278,12 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
elaborateMapCenter()
}
func reloadMap() {
// remove and re-add annotations, to force redrawn
mapView.removeAnnotations(mapAnnotations)
mapView.addAnnotations(mapAnnotations)
}
/// Changes the center coordinate of the map to a given location
func setMapCenter(for location: CLLocation, span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 8, longitudeDelta: 8)) {
let region = MKCoordinateRegion(center: location.coordinate, span: span)
@@ -249,6 +295,31 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
// nope, subclass will implement logic
}
func zPriority(for annotation: MKAnnotation) -> MKAnnotationViewZPriority {
// subclass will impelement its own logic to define annotation priority
.min
}
func createSnapshot(
prepare: () -> Void = { },
restore: () -> Void = { }
) -> UIImage {
prepare()
addWatermarkView()
// riduciamo la porzione da salvare alla sola mappa (eliminiamo i filtri)
let size = CGSize(width: view.bounds.width, height: mapView.bounds.maxY)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { ctx in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
restore()
removeWatermarkView()
return image
}
// MARK: - Private
private func updateUI() {
@@ -286,6 +357,7 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
}
let annotationView = setupAnnotationView(for: annotation, on: mapView)
annotationView?.zPriority = zPriority(for: annotation)
return annotationView
}
@@ -244,7 +244,7 @@ class AlertSimulatorViewController: UIViewController, MKMapViewDelegate {
}
private func navigateToSubscriptions() {
let controller = SubscriptionsViewController.makeViewController()
let controller = SubscriptionsViewController()
let navigationController = UINavigationController(rootViewController: controller)
present(navigationController, animated: true)
}
-2
View File
@@ -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
@@ -6,16 +6,12 @@
#import "Costanti.h"
#import "EQNUser.h"
#import "EQNManager.h"
#import "EQNNotificheReteSismiche.h"
#import "EQNNotificheSegnalazioniUtente.h"
#import "EQNSisma.h"
#import "EQNBaseViewController.h"
#import "SettingsBaseViewController.h"
#import "EQNGeneratoreURLServer.h"
#import "ServerRequest.h"
#import "EQNSegnalazione.h"
#import "EQNPastquakes.h"
#import "EQNAllertaSismica.h"
#import "GADTTemplateView.h"
#import "GADTMediumTemplateView.h"
+6 -4
View File
@@ -55,9 +55,9 @@
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCalendarsUsageDescription</key>
<string>L'accesso al calendario è richiesto per poter salvare le informazioni dei sismi di interesse</string>
<string>L&apos;accesso al calendario è richiesto per poter salvare le informazioni dei sismi di interesse</string>
<key>NSContactsUsageDescription</key>
<string>L'accesso ai contatti è richiesto per poter aggiungere persone agli eventi creati</string>
<string>L&apos;accesso ai contatti è richiesto per poter aggiungere persone agli eventi creati</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string> Ci occorre la tua posizione per inviare messaggi precisi in caso di terremoto</string>
<key>NSLocationAlwaysUsageDescription</key>
@@ -65,9 +65,9 @@
<key>NSLocationWhenInUseUsageDescription</key>
<string> Ci occorre la tua posizione per inviare messaggi precisi in caso di terremoto</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>L'accesso alla libreria è richiesto per poter salvare le immagini generate dall'app</string>
<string>L&apos;accesso alla libreria è richiesto per poter salvare le immagini generate dall&apos;app</string>
<key>NSUserTrackingUsageDescription</key>
<string>Il tracciamento serve a capire se la pubblicità dell'app è efficace</string>
<string>Il tracciamento serve a capire se la pubblicità dell&apos;app è efficace</string>
<key>SKAdNetworkItems</key>
<array>
<dict>
@@ -104,5 +104,7 @@
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
</dict>
</plist>
@@ -26,3 +26,24 @@ extension NSDate {
return (self as Date).isBeforeInterval(interval)
}
}
extension CGFloat {
var negative: CGFloat {
-self
}
var x2: CGFloat {
self*2.0
}
}
extension CaseIterable where Self: Equatable {
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
}
}
@@ -12,9 +12,7 @@ import UIKit
extension UIView {
func eqn_applyShadowAndRoundedCorners() {
// rounded corners
layer.cornerRadius = AppTheme.shared.cardCornerRadius
layer.masksToBounds = false
eqn_applyRoundedCorners()
// apply a shadow to the current view
layer.shadowColor = UIColor.black.cgColor
@@ -22,4 +20,10 @@ extension UIView {
layer.shadowOffset = CGSize(width: 0, height: 2)
layer.shadowRadius = 2
}
func eqn_applyRoundedCorners() {
// rounded corners
layer.cornerRadius = AppTheme.shared.cardCornerRadius
layer.masksToBounds = false
}
}
+151
View File
@@ -0,0 +1,151 @@
//
// Log.swift
// Earthquake Network
//
// Created by Andrea Busi on 27/02/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import Foundation
import OSLog
/// Use this protocol to have a base TAG in a Swift class
public protocol Loggable {
static var TAG: String { get }
}
public extension Loggable {
static var TAG: String {
String(describing: Self.self)
}
}
extension UIViewController: Loggable { }
public class Log {
private static let dumpDateFormatter: DateFormatter = {
// create the default date formatter using ISO8601 date format
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return formatter
}()
private static let shared = Log()
// MARK: - Properties
private let maxNumberOfLogsInDump: Int
private let logsLifespanMillis: Int
/// Subsystem for OSLog
private let subsystem: String
/// Logging in everything in a single "APP" category
private let appCategory: String = "APP"
private lazy var logger: os.Logger = {
os.Logger(subsystem: subsystem, category: appCategory)
}()
// MARK: - Init
@objc
public init(
subsystem: String = Bundle.main.bundleIdentifier!,
maxNumberOfLogsInDump: Int = 5000,
logsLifespanMillis: Int = 3 * 24 * 3600 * 1000
) {
self.subsystem = subsystem
self.maxNumberOfLogsInDump = maxNumberOfLogsInDump
self.logsLifespanMillis = logsLifespanMillis
}
// MARK: - Internal
public static func error(tag: String?, _ message: String?, _ functionName: String = #function) {
shared.log(level: .fault, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName)
}
public static func warning(tag: String?, _ message: String?, _ functionName: String = #function) {
shared.log(level: .error, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName)
}
public static func info(tag: String?, _ message: String?, _ functionName: String = #function) {
shared.log(level: .info, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName)
}
public static func debug(tag: String?, _ message: String?, _ functionName: String = #function) {
shared.log(level: .debug, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName)
}
public static func verbose(tag: String?, _ message: String?, _ functionName: String = #function) {
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()) ?? ""
}
// MARK: - Private
private func log(level: OSLogType, tag: String, message: String, functionName: String) {
let formattedMessage = "[\(tag)] \(functionName): \(message)"
switch level {
case .fault: logger.fault("\(formattedMessage, privacy: .public)")
case .error: logger.error("\(formattedMessage, privacy: .public)")
case .default: logger.notice("\(formattedMessage, privacy: .public)")
case .info: logger.info("\(formattedMessage, privacy: .public)")
default: logger.debug("\(formattedMessage, privacy: .public)")
}
}
/// 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()
let text = logs
.compactMap { "\(Self.dumpDateFormatter.string(from: $0.date)) [\($0.level)] \($0.composedMessage)" }
.joined(separator: "\n")
return text
}
return try await logTask.value
}
@available(iOS 15.0, *)
private func retrieveLogEntries() throws -> [OSLogEntryLog] {
// Open the log store.
let logStore = try OSLogStore(scope: .currentProcessIdentifier)
// Fetch log objects from the given time interval
let intervalPosition = logStore.position(date: Date().addingTimeInterval(TimeInterval(-logsLifespanMillis / 1000)))
let allEntries = try logStore.getEntries(at: intervalPosition)
// Filter the log to be relevant for our specific subsystem
// and remove other elements (signposts, etc).
return allEntries
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem == subsystem }
.suffix(maxNumberOfLogsInDump)
}
}
@available(iOS 15.0, *)
extension OSLogEntryLog.Level: @retroactive CustomStringConvertible {
public var description: String {
switch self {
case .fault: return "FAULT"
case .error: return "ERROR"
case .notice: return "WARNING"
case .info: return "INFO"
case .debug: return "DEBUG"
case .undefined: return "UNDEFINED"
@unknown default:
return "UNKNOWN"
}
}
}
@@ -29,4 +29,37 @@ class AppPreferences: NSObject {
get { UserDefaults.standard.bool(forKey: UserDefaults.AlertsShowCardOptions) }
set { UserDefaults.standard.set(newValue, forKey: UserDefaults.AlertsShowCardOptions) }
}
var mapPinStyle: MapPinStyle {
get {
let saved = UserDefaults.standard.integer(forKey: UserDefaults.MapPinStyle)
return MapPinStyle(rawValue: saved) ?? .circle
}
set {
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)
}
}
}
@@ -14,7 +14,7 @@ public class EQNAppearanceCommand: EQNCommandProtocol {
// MARK: - Public
func execute() {
print("EQNAppearanceCommand: start execute")
print("[EQNAppearanceCommand] Start execute")
applyAppearance()
}
@@ -32,7 +32,7 @@ public class EQNAppearanceCommand: EQNCommandProtocol {
navAppearance.largeTitleTextAttributes = [
NSAttributedString.Key.foregroundColor: AppTheme.Colors.darkGray
]
navAppearance.backgroundColor = AppTheme.Colors.primary
navAppearance.backgroundColor = AppTheme.Colors.navBar
navAppearance.shadowColor = UIColor.clear
proxyNavBar.isTranslucent = false
@@ -15,67 +15,108 @@ public class EQNUserDefaultsCommand: EQNCommandProtocol {
// MARK: - Public
func execute() {
print("EQNUserDefaultsCommand: start execute")
print("[EQNUserDefaultsCommand] Start execute")
applyDefaultSettings()
saveMissingValues()
migrationV5_3()
migrationV5_4()
migrationV5_8()
migrationFirstAppStat()
migrationCriticalAlerts()
migrationV5_9()
}
// MARK: - Private
private func applyDefaultSettings() {
// seismic card settings
if UserDefaults.standard.array(forKey: EQNUserDefaultKeySesmicInformations) == nil {
let informations: [SeismicNetworkTableViewCell.InformationType] = [.buttons, .distance, .coordinate, .population]
UserDefaults.standard.set(informations.map { $0.rawValue }, forKey: EQNUserDefaultKeySesmicInformations)
}
}
private func saveMissingValues() {
// `raggio sismi forti` was not saved before v2.3
if UserDefaults.standard.object(forKey: UserDefaults.AllertaSismicaRaggioSismiForti) == nil {
UserDefaults.standard.set("600", forKey: UserDefaults.AllertaSismicaRaggioSismiForti)
}
}
private func migrationV5_3() {
let migrationPerformed = UserDefaults.standard.bool(forKey: UserDefaults.AppMigrationV5_3)
private func migrationV5_8() {
let migrationPerformed = UserDefaults.standard.bool(forKey: UserDefaults.AppMigrationV5_8)
if migrationPerformed {
print("[EQNUserDefaultsCommand] Migration v5.3 already performed")
print("[EQNUserDefaultsCommand] Migration v5.8 already performed")
return
}
// l'ultima posizione era salvata come array, la trasformiamo in valore singolo
let lastLocations = EQNUtility.loadArray(of: CLLocation.self, fromUserDefaultsForKey: UserDefaults.UserDataLastLocation) as? [CLLocation]
if let lastLocation = lastLocations?.last {
UserDefaults.standard.removeObject(forKey: UserDefaults.UserDataLastLocation)
EQNUserData.shared.saveLastLocation(lastLocation)
}
// resettiamo il Firebase token in modo da ri-eseguire la procedura di registrazione corretta
EQNUserData.shared.saveFirebaseToken(nil)
UserDefaults.standard.set(true, forKey: UserDefaults.AppMigrationV5_3)
}
private func migrationV5_4() {
let migrationPerformed = UserDefaults.standard.bool(forKey: UserDefaults.AppMigrationV5_4)
if migrationPerformed {
print("[EQNUserDefaultsCommand] Migration v5.4 already performed")
return
}
// migriamo l'ultima posizione negli user defaults condivisi
// delete old notification settings
let userDefaults = UserDefaults.standard
let groupUserDefaults = UserDefaults.appGroup
if let encodedLocation = userDefaults.object(forKey: UserDefaults.UserDataLastLocation) as? Data {
groupUserDefaults?.set(encodedLocation, forKey: UserDefaults.UserDataLastLocation)
[
"NOTIFICHE_ATTIVA_RETI_SISMICHE_VICINE", "NOTIFICHE_ATTIVA_RETI_TERREMOTI_FORTI",
"NOTIFICHE_ATTIVA_RETI_ENERGIA_FORTI", "NOTIFICHE_ATTIVA_RETI_LISTA_ENTI"
].forEach { key in
userDefaults.removeObject(forKey: key)
}
userDefaults.set(true, forKey: UserDefaults.AppMigrationV5_4)
// delete old filter values
[
"EQN_ETA_MASSIMA", "EQN_SISMI_FORTI_ABILITATI", "EQN_SISMI_FORTI",
"EQN_SISMI_QUALSIASI_MAGNITUDO", "EQN_SISMI_MODIFICA_IMPOSTAZIONI"
].forEach { key in
userDefaults.removeObject(forKey: key)
}
// delete old "real time alert" settings
[
"NOTIFICHE_ALLERA_SISMICA_IMPOSTA_VOLUME", "NOTIFICHE_ALLERA_SISMICA_TESTA_ALLARME",
"NOTIFICHE_ALLERA_SISMICA_ABILITA_INTERVALLO", "NOTIFICHE_ALLERA_SISMICA_ORA_INIZIO",
"NOTIFICHE_ALLERA_SISMICA_ORA_INIZIO"
].forEach { key in
userDefaults.removeObject(forKey: key)
}
userDefaults.set(true, forKey: UserDefaults.AppMigrationV5_8)
}
private func migrationFirstAppStat() {
// before v5.8.2, first app start was defined using Firebase Token
let userDefaults = UserDefaults.standard
let firstAppStartExecuted = userDefaults.bool(forKey: UserDefaults.FirstAppStartExecuted)
if firstAppStartExecuted {
print("[EQNUserDefaultsCommand] First app start already executed")
return
}
let firebaseToken = userDefaults.object(forKey: UserDefaults.UserDataFirebaseToken) as? String
if firebaseToken != nil {
print("[EQNUserDefaultsCommand] First app start migrated")
userDefaults.set(true, forKey: UserDefaults.FirstAppStartExecuted)
}
}
private func migrationCriticalAlerts() {
let userDefaults = UserDefaults.standard
let migrationPerformed = userDefaults.bool(forKey: UserDefaults.AppMigrationV5_8_2)
if migrationPerformed {
print("[EQNUserDefaultsCommand] Migration v5.8.2 already performed")
return
}
UNUserNotificationCenter.current().getNotificationSettings { settings in
if settings.criticalAlertSetting != .enabled {
print("[EQNUserDefaultsCommand] Critical alerts not enabled, disable settings")
EQNSettingRealTimeAlert.shared.isCriticalAlertsEnabled = false
EQNSettingRealTimeAlert.shared.saveUserInfo()
} else {
print("[EQNUserDefaultsCommand] Critical alerts enabled, do nothing")
}
}
userDefaults.set(true, forKey: UserDefaults.AppMigrationV5_8_2)
}
private func migrationV5_9() {
let userDefaults = UserDefaults.standard
let migrationPerformed = userDefaults.bool(forKey: UserDefaults.AppMigrationV5_9)
if migrationPerformed {
print("[EQNUserDefaultsCommand] Migration v5.9 already performed")
return
}
// 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)
}
}
+95 -84
View File
@@ -10,89 +10,121 @@ import Foundation
@objc class EQNData: NSObject {
@objc public static let MaxRaggioSisma = "100000"
@objc public static let DefaultRaggioSisma = EQNGenericValue(value:MaxRaggioSisma, display:"radius_any_distance")
@objc public static let DefaultMagitudoDebole = EQNGenericValue(value:"2.0", display:"official_magnitude_value_20")
@objc public static let DefaultMagitudoForte = EQNGenericValue(value:"5.5", display:"official_magnitude_value_55")
@objc public static let DefaultPeriodoTemporale = EQNGenericValue(value: "1440", display: "report_timeframe_one_day")
static let MaxRaggioSisma = "100000"
static let DefaultSettingSeismicNetworkNotificationRadius = EQNGenericValue(value:"500", display:"500 km")
static let DefaultSettingSeismicNetworkNotificationMagitude = EQNGenericValue(value:"2.0", display:"official_magnitude_value_20")
static let DefaultSettingUserReportNotificationRadius = EQNGenericValue(value:"1000", display:"1000 km")
static let DefaultFilterRadius = EQNGenericValue(value:"250", display:"250 km")
static let DefaultFilterMagnitude = EQNGenericValue(value:"0.0", display:"official_magnitude_value_00")
// MARK: - Public
@objc class func raggioSismi() -> [EQNGenericValue] {
[
EQNGenericValue(value:"50", display:"50 km"),
EQNGenericValue(value:"100", display:"100 km"),
EQNGenericValue(value:"200", display:"200 km"),
EQNGenericValue(value:"300", display:"300 km"),
EQNGenericValue(value:"400", display:"400 km"),
EQNGenericValue(value:"500", display:"500 km"),
EQNGenericValue(value:"600", display:"600 km"),
EQNGenericValue(value:"800", display:"800 km"),
EQNGenericValue(value:"1000", display:"1000 km"),
EQNGenericValue(value:"2000", display:"2000 km"),
EQNGenericValue(value:"4000", display:"4000 km"),
EQNGenericValue(value:Self.MaxRaggioSisma, display:"radius_any_distance"),
]
}
// Distances for "seismic network notifications"
static let settingSeismicNetworkNotificationRadius: [EQNGenericValue] = [
EQNGenericValue(value:"100", display:"100 km"),
EQNGenericValue(value:"250", display:"250 km"),
EQNGenericValue(value:"500", display:"500 km"),
EQNGenericValue(value:"1000", display:"1000 km")
]
// Magnitudes for "seismic network notifications"
static let settingSeismicNetworkNotificationMagnitudes: [EQNGenericValue] = [
EQNGenericValue(value:"0.0", display:"0.0"),
EQNGenericValue(value:"0.5", display:"0.5"),
EQNGenericValue(value:"1.0", display:"1.0"),
EQNGenericValue(value:"1.5", display:"1.5"),
EQNGenericValue(value:"2.0", display:"2.0"),
EQNGenericValue(value:"2.5", display:"2.5"),
EQNGenericValue(value:"3.0", display:"3.0"),
EQNGenericValue(value:"3.5", display:"3.5"),
EQNGenericValue(value:"4.0", display:"4.0"),
EQNGenericValue(value:"4.5", display:"4.5"),
EQNGenericValue(value:"5.0", display:"5.0"),
EQNGenericValue(value:"5.5", display:"5.5")
]
// Distances for "user report notifications"
static let settingUserReportNotificationRadius: [EQNGenericValue] = [
EQNGenericValue(value:"100", display:"100 km"),
EQNGenericValue(value:"250", display:"250 km"),
EQNGenericValue(value:"500", display:"500 km"),
EQNGenericValue(value:"1000", display:"1000 km")
]
// Misure raggio utilizzate nei filtri
static let filterRadius: [EQNGenericValue] = [
EQNGenericValue(value:"100", display:"100 km"),
EQNGenericValue(value:"250", display:"250 km"),
EQNGenericValue(value:"500", display:"500 km"),
EQNGenericValue(value:"750", display:"750 km"),
EQNGenericValue(value:"1000", display:"1000 km"),
EQNGenericValue(value:"1500", display:"1500 km"),
EQNGenericValue(value:"2000", display:"2000 km")
]
// Magnitudo utilizzate nei filtri
static let filterMagnitude: [EQNGenericValue] = [
EQNGenericValue(value:"0.0", display:"0.0"),
EQNGenericValue(value:"1.0", display:"1.0"),
EQNGenericValue(value:"2.0", display:"2.0"),
EQNGenericValue(value:"3.0", display:"3.0"),
EQNGenericValue(value:"4.0", display:"4.0"),
EQNGenericValue(value:"5.0", display:"5.0"),
EQNGenericValue(value:"6.0", display:"6.0")
]
/// Returns the EQNGenericValue for the given value, or the default one
/// - Parameter value: Sisma value to search
/// - Parameter value: Sisma radius to search
/// - Returns: Found value or default
@objc class func raggioSisma(for value: String?) -> EQNGenericValue {
if let value = value, let genericValue = Self.raggioSismi().first(where: { $0.value == value }) {
@objc(getSettingSeismicNetworkAlertRadiusForValue:)
class func getSettingSeismicNetworkNotificationRadius(for value: String?) -> EQNGenericValue {
if let value = value, let genericValue = settingSeismicNetworkNotificationRadius.first(where: { $0.value == value }) {
return genericValue
}
return Self.DefaultRaggioSisma
}
@objc class func magitudoDeboli() -> [EQNGenericValue] {
[
EQNGenericValue(value:"0.0", display:"official_magnitude_value_00"),
EQNGenericValue(value:"0.5", display:"official_magnitude_value_05"),
EQNGenericValue(value:"1.0", display:"official_magnitude_value_10"),
EQNGenericValue(value:"1.5", display:"official_magnitude_value_15"),
EQNGenericValue(value:"2.0", display:"official_magnitude_value_20"),
EQNGenericValue(value:"2.5", display:"official_magnitude_value_25"),
EQNGenericValue(value:"3.0", display:"official_magnitude_value_30"),
EQNGenericValue(value:"3.5", display:"official_magnitude_value_35"),
EQNGenericValue(value:"4.0", display:"official_magnitude_value_40"),
EQNGenericValue(value:"4.5", display:"official_magnitude_value_45"),
EQNGenericValue(value:"5.0", display:"official_magnitude_value_50"),
EQNGenericValue(value:"5.5", display:"official_magnitude_value_55")
]
return DefaultSettingSeismicNetworkNotificationRadius
}
/// Returns the EQNGenericValue for the given value, or the default one
/// - Parameter value: Magnitudo value to search
/// - Returns: Found value or default
@objc class func magitudoDebole(for value: String?) -> EQNGenericValue {
if let value = value, let genericValue = Self.magitudoDeboli().first(where: { $0.value == value }) {
class func getSettingSeismicNetworkNotificationMagnitudes(for value: String?) -> EQNGenericValue {
if let value = value, let genericValue = settingSeismicNetworkNotificationMagnitudes.first(where: { $0.value == value }) {
return genericValue
}
return Self.DefaultMagitudoDebole
}
@objc class func magitudoForti() -> [EQNGenericValue] {
[
EQNGenericValue(value:"5.5", display:"official_magnitude_value_55"),
EQNGenericValue(value:"6.0", display:"official_magnitude_value_60"),
EQNGenericValue(value:"6.5", display:"official_magnitude_value_65"),
EQNGenericValue(value:"7.0", display:"official_magnitude_value_70"),
EQNGenericValue(value:"7.5", display:"official_magnitude_value_75")
]
return DefaultSettingSeismicNetworkNotificationMagitude
}
/// Returns the EQNGenericValue for the given value, or the default one
/// - Parameter value: Magnitudo value to search
/// - Parameter value: Sisma radius to search
/// - Returns: Found value or default
@objc class func magitudoForte(for value: String?) -> EQNGenericValue {
if let value = value, let genericValue = Self.magitudoForti().first(where: { $0.value == value }) {
class func getSettingUserReportNotificationRadius(for value: String?) -> EQNGenericValue {
if let value = value, let genericValue = settingUserReportNotificationRadius.first(where: { $0.value == value }) {
return genericValue
}
return Self.DefaultMagitudoForte
return DefaultSettingUserReportNotificationRadius
}
@objc class func seismicNetworks() -> [EQNSeismicNetwork] {
/// Returns the EQNGenericValue for the given value, or the default one
/// - Parameter value: Sisma radius to search
/// - Returns: Found value or default
class func filterRadius(for value: String?) -> EQNGenericValue {
if let value = value, let genericValue = filterRadius.first(where: { $0.value == value }) {
return genericValue
}
return DefaultFilterRadius
}
/// Returns the EQNGenericValue for the given value, or the default one
/// - Parameter value: Magnitudo value to search
/// - Returns: Found value or default
class func filterMagnitude(for value: String?) -> EQNGenericValue {
if let value = value, let genericValue = filterMagnitude.first(where: { $0.value == value }) {
return genericValue
}
return DefaultFilterMagnitude
}
class func seismicNetworks() -> [EQNSeismicNetwork] {
[
EQNSeismicNetwork(acronym: "USGS", country: NSLocalizedString("configuration_countries_united_states", comment: ""), extended: ""),
EQNSeismicNetwork(acronym: "INGV", country: NSLocalizedString("configuration_countries_italy", comment: ""), extended: ""),
@@ -120,37 +152,16 @@ import Foundation
]
}
@objc class func seismicNetworkAcronyms() -> [String] {
class func seismicNetworkAcronyms() -> [String] {
Self.seismicNetworks().map { $0.acronym }
}
@objc class func seismicNetworkCountries() -> [String] {
class func seismicNetworkCountries() -> [String] {
Self.seismicNetworks().map { $0.country }
}
@objc class func seismic(for acronym: String?) -> EQNSeismicNetwork? {
class func seismic(for acronym: String?) -> EQNSeismicNetwork? {
guard let acronym = acronym else { return nil }
return Self.seismicNetworks().first(where: { $0.acronym == acronym })
}
@objc class func periodiTemporali() -> [EQNGenericValue] {
[
EQNGenericValue(value: "10", display: "10 minuti"),
EQNGenericValue(value: "60", display: "report_timeframe_one_hour"),
EQNGenericValue(value: "120", display: "report_timeframe_two_hours"),
EQNGenericValue(value: "360", display: "report_timeframe_six_hours"),
EQNGenericValue(value: "720", display: "report_timeframe_twelve_hours"),
EQNGenericValue(value: "1440", display: "report_timeframe_one_day")
]
}
/// Returns the EQNGenericValue for the given value, or the default one
/// - Parameter value: Temporal unit value to search
/// - Returns: Found value or default
@objc class func periodoTemporale(for value: String?) -> EQNGenericValue {
if let value = value, let genericValue = Self.periodiTemporali().first(where: { $0.value == value }) {
return genericValue
}
return Self.DefaultPeriodoTemporale
}
}
@@ -27,10 +27,110 @@
/// THE SOFTWARE.
import Foundation
import StoreKit
public struct VersioneProProducts {
public struct EQNInAppProducts {
public enum Identifier {
enum Plan: CaseIterable {
case monthly
case yearly
case perpetual
var localizedTitle: String {
switch self {
case .monthly: NSLocalizedString("subscription_plan_monthly", comment: "")
case .yearly: NSLocalizedString("subscription_plan_yearly", comment: "")
case .perpetual: NSLocalizedString("subscription_plan_perpetual", comment: "")
}
}
}
enum Category {
case pro
case top10k
case top100k
var image: UIImage? {
switch self {
case .pro: nil
case .top10k: UIImage(named: "top_10k")
case .top100k: UIImage(named: "top_100k")
}
}
var localizedTitle: String {
switch self {
case .pro: return NSLocalizedString("network_pro", comment: "")
case .top10k: return "Top 10k"
case .top100k: return "Top 100k"
}
}
}
let plan: Plan
let category: Category
let isDiscounted: Bool
let product: SKProduct
// MARK: - Init
private init(plan: Plan, category: Category, isDiscounted: Bool, product: SKProduct) {
self.plan = plan
self.category = category
self.isDiscounted = isDiscounted
self.product = product
}
// MARK: - Accessories
var isTop10k: Bool {
category == .top10k
}
var isTop100k: Bool {
category == .top100k
}
var isSubscription: Bool {
category != .pro
}
var productIdentifier: String {
product.productIdentifier
}
// MARK: - Static
static func from(product: SKProduct) -> EQNInAppProducts? {
switch product.productIdentifier {
case Identifier.ProVersionFullPrice:
.init(plan: .perpetual, category: .pro, isDiscounted: false, product: product)
case Identifier.ProVersionDiscounted:
.init(plan: .perpetual, category: .pro, isDiscounted: true, product: product)
case Identifier.Subscription10kMonthly:
.init(plan: .monthly, category: .top10k, isDiscounted: false, product: product)
case Identifier.Subscription10kYearly:
.init(plan: .yearly, category: .top10k, isDiscounted: false, product: product)
case Identifier.Subscription10kYearlyDiscounted:
.init(plan: .yearly, category: .top10k, isDiscounted: true, product: product)
case Identifier.Subscription10kPerpetual:
.init(plan: .perpetual, category: .top10k, isDiscounted: false, product: product)
case Identifier.Subscription100kMonthly:
.init(plan: .monthly, category: .top100k, isDiscounted: false, product: product)
case Identifier.Subscription100kYearly:
.init(plan: .yearly, category: .top100k, isDiscounted: false, product: product)
case Identifier.Subscription100kYearlyDiscounted:
.init(plan: .yearly, category: .top100k, isDiscounted: true, product: product)
case Identifier.Subscription100kPerpetual:
.init(plan: .perpetual, category: .top100k, isDiscounted: false, product: product)
default:
nil
}
}
enum Identifier {
static let ProVersionFullPrice = "com.finazzi.distquake.ProPrezzoPieno"
static let ProVersionDiscounted = "com.finazzi.distquake.VersioneProScontata"
@@ -66,40 +166,9 @@ public struct VersioneProProducts {
static let identifiersForTop100k: Set<ProductIdentifier> = [
Subscription100kMonthly, Subscription100kYearly, Subscription100kYearlyDiscounted, Subscription100kPerpetual
]
static let identifierForSubscriptions: Set<ProductIdentifier> = [
Subscription10kMonthly, Subscription100kMonthly,
Subscription10kYearly, Subscription10kYearlyDiscounted,
Subscription100kYearly, Subscription100kYearlyDiscounted,
Subscription10kPerpetual, Subscription100kPerpetual
]
}
static func isSubscription(for identifier: String) -> Bool {
Identifier.identifierForSubscriptions.contains(identifier)
}
static func is10kSubscription(for identifier: String) -> Bool {
Identifier.identifiersForTop10k.contains(identifier)
}
static func is100kSubscription(for identifier: String) -> Bool {
Identifier.identifiersForTop100k.contains(identifier)
}
static func image(for productIdentifier: String) -> UIImage? {
if is100kSubscription(for: productIdentifier){
return UIImage(named: "top_100k")
}
if is10kSubscription(for: productIdentifier) {
return UIImage(named: "top_10k")
}
return nil
}
public static let store = IAPHelper(productIds: VersioneProProducts.Identifier.identifiers)
public static let store = IAPHelper(productIds: EQNInAppProducts.Identifier.identifiers)
}
func resourceNameForProductIdentifier(_ productIdentifier: String) -> String? {
+8 -12
View File
@@ -120,19 +120,15 @@
- (void)scaricaReteSismica
{
// L'endpoint per lo scaricamento dei dati prende due parametri: pro per il provider selezionato, mag per la
// magnitudo minima. Se l'utente ha selezionato più di un provider, inviamo ALL. Mentre la magnitudo
// deve avere sempre una cifra decimale (es 2.0).
NSString *filterProvider = @"";
NSArray<NSString *> *networks = [EQNUserData.sharedData seismicNetworksSelected];
if (networks.count == 1) {
filterProvider = [networks firstObject];
} else {
filterProvider = @"ALL";
}
// 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".
// Per la magnitudo minima, invece, passiamo 0 per i filtri "raggio" e "rilevanti,
// altrimenti passiamo 2 per il filtro "mondo"
NSString *filterMagnitude = [EQNSeismic shared].magnitudoMinima;
NSString *filterProvider = @"ALL";
NSString *filterMagnitude = [EQNSeismic shared].filterOption == FilterTypeWorldWide ? @"2.0" : @"0.0";
NSString *queryString = [NSString stringWithFormat:@"?pro=%@&mag=%@", filterProvider, filterMagnitude];
NSString *urlString = [NSString stringWithFormat:EQNServerUrlDownloadRetiSismiche, queryString];
@@ -0,0 +1,130 @@
//
// EQNOfficialPushNotification.swift
// Earthquake Network
//
// Created by Andrea Busi on 29/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import Foundation
import Shogun
@objc
class EQNOfficialPushNotification: NSObject, Codable {
private enum CodingKeys: String, CodingKey {
case latitude
case longitude
case magnitude
case date
}
private let latitude: Double
private let longitude: Double
let magnitude: Double
let date: Date?
var coordinate: CLLocation {
.init(latitude: latitude, longitude: longitude)
}
// MARK: - Init
init(
latitude: Double,
longitude: Double,
magnitude: Double,
date: Date?
) {
self.latitude = latitude
self.longitude = longitude
self.magnitude = magnitude
self.date = date
}
// MARK: - Class
/// Remove any saved notification
static func removeStored() {
UserDefaults.standard.removeObject(forKey: UserDefaults.OfficialAlertPayload)
}
/// Retrieve stored notification (if any)
static func stored() -> EQNOfficialPushNotification? {
guard let data = UserDefaults.standard.object(forKey: UserDefaults.OfficialAlertPayload) as? Data else {
print("[EQNOfficialPushNotification] No notification saved for key '\(UserDefaults.OfficialAlertPayload)'")
return nil
}
guard let notification = try? JSONDecoder().decode(EQNOfficialPushNotification.self, from: data) else {
print("[EQNOfficialPushNotification] Unable to decode given notification")
return nil
}
return notification
}
/// Convert and store a push notification payload.
/// Expected payload has the following structure:
/// ```
/// {
/// "title": "Segnalazione da rete sismica",
/// "body": "Sisma rilevato a 4km S Valfabbrica (PG)",
/// "userInfo": {
/// "data" : "2024-06-29 11:21:30",
/// ...
/// "aps": {
/// "alert" : {
/// "loc-key" : "Sisma rilevato a",
/// "title-loc-key" : "Segnalazione da rete sismica",
/// "title" : "Segnalazione da rete sismica",
/// "action-loc-key" : "",
/// "loc-args" : [
/// "6 km S Acate (RG) - M1.9"
/// ]
/// },
/// }
/// }
/// }
/// ```
/// - Parameter payload: Notification payload
/// - Returns: `true` if save succeed, `false` otherwise
@objc(storeNotificationWithPayload:)
@discardableResult
static func store(payload: [String: Any]) -> Bool {
guard let notification = from(payload: payload) else {
print("[EQNOfficialPushNotification] Unable to convert received notification")
return false
}
guard let data = try? JSONEncoder().encode(notification) else {
print("[EQNOfficialPushNotification] Unable to encode given notification")
return false
}
UserDefaults.standard.set(data, forKey: UserDefaults.OfficialAlertPayload)
return true
}
@objc
private static func from(payload: [String: Any]) -> EQNOfficialPushNotification? {
guard let userInfo = payload["userInfo"] as? [String: Any] else {
print("[EQNOfficialPushNotification] Missing required info to parse push notification")
return nil
}
let latitude = userInfo.double(forKey: "latitude") ?? 0
let longitude = userInfo.double(forKey: "longitude") ?? 0
let magnitude = userInfo.double(forKey: "magnitude") ?? 0
let dateString = userInfo.string(forKey: "data") ?? ""
let date = EQNUtility.getDateFrom(dateString)
return .init(
latitude: latitude,
longitude: longitude,
magnitude: magnitude,
date: date
)
}
}
@@ -57,21 +57,21 @@ public class EQNPurchaseUtility: NSObject {
/// Check if user has bought pro app version
/// Pro version is enabled also if a yearly subscription is enabled
@objc public static func isProVersionEnabled() -> Bool {
VersioneProProducts.Identifier.identifierForProVersion.reduce(false) { (result, identifier) -> Bool in
EQNInAppProducts.Identifier.identifierForProVersion.reduce(false) { (result, identifier) -> Bool in
return result || UserDefaults.standard.bool(forKey: identifier)
}
}
/// Check if user has bought Top 10k subscription
@objc public static func isTop10kEnabled() -> Bool {
VersioneProProducts.Identifier.identifiersForTop10k.reduce(false) { (result, identifier) -> Bool in
EQNInAppProducts.Identifier.identifiersForTop10k.reduce(false) { (result, identifier) -> Bool in
return result || UserDefaults.standard.bool(forKey: identifier)
}
}
/// Check if user has bought Top 100k subscription
@objc public static func isTop100kEnabled() -> Bool {
VersioneProProducts.Identifier.identifiersForTop100k.reduce(false) { (result, identifier) -> Bool in
EQNInAppProducts.Identifier.identifiersForTop100k.reduce(false) { (result, identifier) -> Bool in
return result || UserDefaults.standard.bool(forKey: identifier)
}
}
@@ -79,7 +79,7 @@ public class EQNPurchaseUtility: NSObject {
/// Remove saved in-app purchases flags.
/// Used only during development
@objc public static func resetInAppPurchases() {
VersioneProProducts.Identifier.identifiers.forEach { (identifier) in
EQNInAppProducts.Identifier.identifiers.forEach { (identifier) in
UserDefaults.standard.removeObject(forKey: identifier)
}
NotificationCenter.default.post(name: .EQNInAppPurchaseDidComplete, object: nil)
@@ -11,31 +11,37 @@ import Foundation
@objc class EQNSeismic: NSObject {
@objc enum FilterType: Int {
case inRadius
case positionRelevant
case worldWide
case userFelt
}
enum Sort: Int {
case time
case position
case magnitude
}
@objc static let shared = EQNSeismic()
@objc var magnitudoMinima: String
@objc var distanzaMassima: String
@objc var periodoTemporale: String
@objc var sismiFortiAbilitati: Bool
@objc var sismiFortiMagnitudo: String
@objc var sismiQualsiasiAbilitati: Bool
@objc var modificaImpostazioniAbilitato: Bool
@objc var filterOption: FilterType
var sort: Sort
var maximumDistance: String
@objc var minimumMagnitude: String
// MARK: - Init
override init() {
Self.migrateOldDistanza()
Self.migrateOldPeriodo()
Self.migrate_v5_8()
let defaults = UserDefaults.standard
magnitudoMinima = defaults.object(forKey: UserDefaults.SeismicMagnitudoMinima, or: EQNData.DefaultMagitudoDebole.value)
distanzaMassima = defaults.object(forKey: UserDefaults.SeismicDistanzaMassima, or: EQNData.DefaultRaggioSisma.value)
periodoTemporale = defaults.object(forKey: UserDefaults.SeismicEtaMassima, or: EQNData.DefaultPeriodoTemporale.value)
sismiFortiAbilitati = defaults.object(forKey: UserDefaults.SeismicSismiFortiAbilitati, or: false)
sismiFortiMagnitudo = defaults.object(forKey: UserDefaults.SeismicSismiForti, or: EQNData.DefaultMagitudoForte.value)
sismiQualsiasiAbilitati = defaults.object(forKey: UserDefaults.SeismicSismiQualsiasiMagnitudo, or: false)
modificaImpostazioniAbilitato = defaults.object(forKey: UserDefaults.SeismicModificaImpostazioni, or: true)
filterOption = defaults.enumObject(forKey: UserDefaults.SeismicFilterOption, or: .positionRelevant)
sort = defaults.enumObject(forKey: UserDefaults.SeismicSort, or: .time)
maximumDistance = defaults.object(forKey: UserDefaults.SeismicDistanzaMassima, or: EQNData.DefaultFilterRadius.value)
minimumMagnitude = defaults.object(forKey: UserDefaults.SeismicMagnitudoMinima, or: EQNData.DefaultFilterMagnitude.value)
super.init()
}
@@ -44,114 +50,141 @@ import Foundation
// MARK: - Public
public func saveFilters() {
UserDefaults.standard.set(magnitudoMinima, forKey: UserDefaults.SeismicMagnitudoMinima)
UserDefaults.standard.set(distanzaMassima, forKey: UserDefaults.SeismicDistanzaMassima)
UserDefaults.standard.set(periodoTemporale, forKey: UserDefaults.SeismicEtaMassima)
UserDefaults.standard.set(sismiFortiMagnitudo, forKey: UserDefaults.SeismicSismiForti)
UserDefaults.standard.set(sismiFortiAbilitati, forKey: UserDefaults.SeismicSismiFortiAbilitati)
UserDefaults.standard.set(sismiQualsiasiAbilitati, forKey: UserDefaults.SeismicSismiQualsiasiMagnitudo)
UserDefaults.standard.set(modificaImpostazioniAbilitato, forKey: UserDefaults.SeismicModificaImpostazioni)
let defaults = UserDefaults.standard
defaults.set(filterOption.rawValue, forKey: UserDefaults.SeismicFilterOption)
defaults.set(sort.rawValue, forKey: UserDefaults.SeismicSort)
defaults.set(maximumDistance, forKey: UserDefaults.SeismicDistanzaMassima)
defaults.set(minimumMagnitude, forKey: UserDefaults.SeismicMagnitudoMinima)
}
// MARK: - Private
private static func migrateOldDistanza() {
guard let savedValue = UserDefaults.standard.object(forKey: UserDefaults.SeismicDistanzaMassima) as? String else {
print("[EQNSeismic] Distanza massima: nessun valore da convertire")
private class func migrate_v5_8() {
let defaults = UserDefaults.standard
let alreadyMigrated = defaults.bool(forKey: UserDefaults.SismicFiltersMigrationV5_8)
if alreadyMigrated {
return
}
if savedValue.lowercased() == NSLocalizedString("radius_any_distance", comment: "").lowercased() {
print("[EQNSeismic] Distanza massima: trovato qualsiasi distanza, salvo valore")
UserDefaults.standard.set("100000", forKey: UserDefaults.SeismicDistanzaMassima)
} else {
print("[EQNSeismic] Distanza massima: valore da non convertire (value: \(savedValue))")
}
}
private static func migrateOldPeriodo() {
guard let savedValue = UserDefaults.standard.object(forKey: UserDefaults.SeismicEtaMassima) as? String else {
print("[EQNSeismic] Età massima: nessun valore da convertire");
return
if let savedMagnitude = defaults.object(forKey: UserDefaults.SeismicMagnitudoMinima) as? String {
let minMagnitude = switch savedMagnitude {
case "0.0": "0.0"
case "0.5", "1.0": "1.0"
case "1.5", "2.0": "2.0"
case "2.5", "3.0": "3.0"
case "3.5", "4.0": "4.0"
case "4.5", "5.0": "5.0"
case "5.5", "6.0": "6.0"
default: "0.0"
}
defaults.set(minMagnitude, forKey: UserDefaults.SeismicMagnitudoMinima)
}
var convertedValue: String?
if savedValue.lowercased() == NSLocalizedString("report_timeframe_one_day", comment: "").lowercased() {
convertedValue = "1440"
} else if savedValue.lowercased() == NSLocalizedString("report_timeframe_twelve_hours", comment: "").lowercased() {
convertedValue = "720"
} else if savedValue.lowercased() == NSLocalizedString("report_timeframe_six_hours", comment: "").lowercased() {
convertedValue = "360"
} else if savedValue.lowercased() == NSLocalizedString("report_timeframe_two_hours", comment: "").lowercased() {
convertedValue = "120"
} else if savedValue.lowercased() == NSLocalizedString("report_timeframe_one_hour", comment: "").lowercased() {
convertedValue = "60"
} else if savedValue.lowercased() == NSLocalizedString("report_timeframe_ten_minutes", comment: "").lowercased() {
convertedValue = "10"
if let savedDistance = defaults.object(forKey: UserDefaults.SeismicDistanzaMassima) as? String {
let maxDistance = switch savedDistance {
case "50", "100": "100"
case "200", "300": "250"
case "400", "500", "600": "500"
case "800": "750"
case "1000": "1000"
default:
// 2000, 4000, qualsiasi distanza
"2000"
}
defaults.set(maxDistance, forKey: UserDefaults.SeismicDistanzaMassima)
}
if let convertedValue = convertedValue {
print("[EQNSeismic] Età massima: salvo valore convertito (old: \(savedValue) - new: \(convertedValue)")
UserDefaults.standard.set(convertedValue, forKey: UserDefaults.SeismicEtaMassima)
} else {
print("[EQNSeismic] Età massima: valore già convertito")
}
defaults.set(true, forKey: UserDefaults.SismicFiltersMigrationV5_8)
}
// MARK: - Class
@objc func filterSeismicList(_ list: [EQNSisma]) -> [EQNSisma] {
// enti abilitati
var networks = EQNUserData.shared.seismicNetworksSelected()
if networks.isEmpty {
networks = EQNData.seismicNetworkAcronyms()
}
networks = networks.map { $0.lowercased() }
// filtri
let filterDistance = Double(distanzaMassima)
let filterMagnitude = Double(magnitudoMinima)
let filterShowNear = sismiQualsiasiAbilitati
let filterShowNearRadius: Double = 50.0
let filterStrongEarthquake = Double(sismiFortiMagnitudo)
let filterStrongEarthquakeEnabled = sismiFortiAbilitati
let filterTime = Double(periodoTemporale)
let filter_radius = Double(maximumDistance)
let filter_min_magnitude = Double(minimumMagnitude)
// filter seismic list
var filtered = [EQNSisma]()
for seismic in list {
for (i, seismic) in list.enumerated() {
var keep = true
if !networks.contains(seismic.provider.lowercased()) {
keep = false
}
let latitude = seismic.coordinate.coordinate.latitude
let longitude = seismic.coordinate.coordinate.longitude
let provider = seismic.provider.uppercased()
// filtro distanza massima
if let filterDistance = filterDistance, seismic.userDistance > filterDistance {
keep = false
}
// filtro magnitudo minima e mostra sismi di qualsiasi magnitudo
if let filterMagnitude = filterMagnitude, seismic.magnitude.doubleValue < filterMagnitude {
if !filterShowNear {
keep = false
} else {
if seismic.userDistance > filterShowNearRadius {
keep = false
//Ricerca di sismi duplicati in lista
for j in -3...3 {
if (j != 0) && (i + j > 0) && (i+j < list.count) {
let seismic_j = list[i+j]
let latitude_j = seismic_j.coordinate.coordinate.latitude
let longitude_j = seismic_j.coordinate.coordinate.longitude
let provider_j = seismic_j.provider.uppercased()
let delta_lat = abs(latitude - latitude_j)
let delta_lon = abs(longitude - longitude_j)
let delta_time = abs(EQNUtility.getDeltaMinute(seismic.date ?? Date()) - EQNUtility.getDeltaMinute(seismic_j.date ?? Date()))
let ratio = seismic.magnitude.doubleValue/seismic_j.magnitude.doubleValue
if ratio > 0.8 && ratio < 1.2 && delta_lat < 0.5 && delta_lon < 0.5 && delta_time <= 2 {
if provider == "EMSC" {
keep = false
}
if provider == "USGS" && !(provider_j == "EMSC") {
keep = false
}
}
}
}
// filtro sismi forti
if let filterStrongEarthquake = filterStrongEarthquake, seismic.provider == "EMSC" && filterStrongEarthquakeEnabled && seismic.magnitude.doubleValue > filterStrongEarthquake {
keep = true
}
// filtro tempo
if let filterTime = filterTime, seismic.timeDifference > filterTime {
keep = false
if keep {
let distance = seismic.userDistance
let magnitude = seismic.magnitude.doubleValue
if filterOption == .inRadius {
//filtro basato su magnitudo e raggio
if let filter_radius, let filter_min_magnitude, distance > filter_radius || magnitude < filter_min_magnitude {
keep = false
}
} else if filterOption == .positionRelevant {
//filtro sismi significativi
if magnitude < 7.0 && distance > 2000 {
keep = false
} else if magnitude < 6.5 && distance > 1600 {
keep = false
} else if magnitude < 6.0 && distance > 1300 {
keep = false
} else if magnitude < 5.5 && distance > 1000 {
keep = false
} else if magnitude < 5.0 && distance > 700 {
keep = false
} else if magnitude < 4.5 && distance > 500 {
keep = false
} else if magnitude < 4.0 && distance > 350 {
keep = false
} else if magnitude < 3.5 && distance > 200 {
keep = false
} else if magnitude < 3.0 && distance > 125 {
keep = false
} else if magnitude < 2.5 && distance > 70 {
keep = false
} else if magnitude < 2.0 && distance > 35 {
keep = false
} else if magnitude < 1.5 && distance > 20 {
keep = false
}
} else if filterOption == .worldWide {
//filtro che mostra tutti i sismi a livello mondiale di magnitudo>=2
if magnitude < 2 {
keep = false
}
} else if filterOption == .userFelt {
//filtro che mostra i sismi segnalati da più di 1 utente
if seismic.userNumber.intValue < 2 {
keep = false
}
}
}
if keep {
@@ -159,6 +192,29 @@ import Foundation
}
}
switch sort {
case .time:
filtered.sort(by: { seismic1, seismic2 in
guard let date1 = seismic1.date else {
return false
}
guard let date2 = seismic2.date else {
return true
}
return date1 > date2
})
case .position:
filtered.sort { seismic1, seismic2 in
seismic1.userDistance < seismic2.userDistance
}
case .magnitude:
filtered.sort { seismic1, seismic2 in
seismic1.magnitude.doubleValue > seismic2.magnitude.doubleValue
}
}
return filtered
}
}
@@ -0,0 +1,15 @@
//
// EQNShakemap.swift
// Earthquake Network
//
// Created by Andrea Busi on 27/02/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import Foundation
struct EQNShakemap: Decodable {
let lat: [Int]
let lon: [Int]
let intensity: Float
}
@@ -33,6 +33,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong) NSNumber *preliminary;
@property (nonatomic, strong) NSNumber *smartphoneNumber;
@property (nonatomic, strong) NSNumber *userNumber;
/// Code to show "intensity map"
@property (nonatomic, strong) NSString *isoCode;
- (instancetype)initWithInfo:(NSDictionary *)info;
@@ -42,6 +42,8 @@
self.preliminary = info[@"py"];
self.smartphoneNumber = info[@"sm"];
self.userNumber = info[@"rp"];
self.isoCode = info[@"iso"];
}
return self;
}
@@ -65,6 +67,7 @@
[encoder encodeObject:self.preliminary forKey:@"preliminary"];
[encoder encodeObject:self.smartphoneNumber forKey:@"smartphoneNumber"];
[encoder encodeObject:self.userNumber forKey:@"userNumber"];
[encoder encodeObject:self.isoCode forKey:@"isoCode"];
}
- (instancetype)initWithCoder:(NSCoder *)decoder
@@ -86,6 +89,7 @@
self.preliminary = [decoder decodeObjectForKey:@"preliminary"];
self.smartphoneNumber = [decoder decodeObjectForKey:@"smartphoneNumber"];
self.userNumber = [decoder decodeObjectForKey:@"userNumber"];
self.isoCode = [decoder decodeObjectForKey:@"isoCode"];
}
return self;
}
@@ -42,6 +42,7 @@
self.registrationInProgress = NO;
self.user_ID = EQNUserData.sharedData.userId;
self.lastPosition = EQNUserData.sharedData.lastLocation;
self.currentFirebaseToken = EQNUserData.sharedData.firebaseToken;
[self registerForLocationUpdates];
}
@@ -122,6 +123,13 @@
return;
}
if (token == nil) {
NSLog(@"[EQNUser] Firebase token is null, abort registration");
self.registrationInProgress = NO;
[[NSNotificationCenter defaultCenter] postNotificationName:EQNServerRegistrationDidFailNotification object:nil];
return;
}
self.registrationInProgress = YES;
NSURL *url = [EQNGeneratoreURLServer urlRegistrazioneFirebaseToken:token existingUserId:userId];
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:url richiesta:EQNTipoChiamataRegistrazione success:^(id result) {
@@ -19,7 +19,15 @@ import CoreLocation
@objc
var isFirstStart: Bool {
firebaseToken == nil
let firstAppStartExecuted = UserDefaults.standard.bool(forKey: UserDefaults.FirstAppStartExecuted)
return !firstAppStartExecuted
}
// MARK: - Permissions
@objc
var locationAuthorizationStatus: CLAuthorizationStatus {
CLLocationManager().authorizationStatus
}
// MARK: - Firebase Token
@@ -61,4 +61,9 @@ extension EQNUtility {
class func formattedDate(from date: Date) -> String {
Self.dateFormatter.string(from: date)
}
/// Convert a date to minutes
class func getDeltaMinute(_ date: Date) -> TimeInterval {
Date().timeIntervalSince(date) / 60.0
}
}
@@ -20,6 +20,7 @@
{
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.timeZone = [NSTimeZone timeZoneWithName:@"Europe/Rome"];
formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US"];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
return [formatter dateFromString:dateString];
}
@@ -6,15 +6,16 @@
// Copyright © 2021 Earthquake Network. All rights reserved.
//
import Foundation
import UIKit
import MapKit
class EQNMapAnnotationSeismic: NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String?
var image: UIImage?
let coordinate: CLLocationCoordinate2D
let title: String?
let subtitle: String?
let textColor: UIColor
let seismic: EQNSisma
@@ -22,58 +23,60 @@ class EQNMapAnnotationSeismic: NSObject, MKAnnotation {
init(seismic: EQNSisma) {
self.seismic = seismic
self.coordinate = CLLocationCoordinate2D(latitude: seismic.coordinate.coordinate.latitude, longitude: seismic.coordinate.coordinate.longitude)
self.image = Self.image(for: seismic)
self.coordinate = CLLocationCoordinate2D(latitude: seismic.coordinate.coordinate.latitude,
longitude: seismic.coordinate.coordinate.longitude)
self.title = seismic.provider
if let date = seismic.date {
self.title = EQNUtility.formattedTimeDifference(from: date)
self.subtitle = EQNUtility.formattedTimeDifference(from: date)
} else {
self.subtitle = nil
}
self.textColor = Self.textColor(for: seismic)
super.init()
}
// MARK: - Helpers
func image(
height: CGFloat,
isUserSelection: Bool
) -> UIImage? {
let color = Self.imageColor(for: seismic)
let borderColor = isUserSelection ? AppTheme.Colors.red : AppTheme.Colors.darkGray
return UIImage.circle(diameter: height, color: color, borderWidth: 2.0, borderColor: borderColor)
}
// MARK: - Private
private class func image(for seismic: EQNSisma) -> UIImage? {
private class func textColor(for seismic: EQNSisma) -> UIColor {
let magnitude = seismic.magnitude.doubleValue
let color: String
if magnitude < 2.0 {
color = "white"
return UIColor(red: 0.0, green: 200.0/255.0, blue: 200.0/255.0, alpha: 1.0)
} else if magnitude < 3.5 {
color = "green"
return UIColor(red: 0.0, green: 200.0/255.0, blue: 0.0, alpha: 1.0)
} else if magnitude < 4.5 {
color = "yellow"
return UIColor(red: 240.0/255.0, green: 190.0/255.0, blue: 0.0, alpha: 1.0)
} else if magnitude < 5.5 {
color = "red"
return UIColor(red: 200.0/255.0, green: 0.0, blue: 0.0, alpha: 1.0)
} else {
color = "purple"
return UIColor(red: 200.0/255.0, green: 0.0, blue: 200.0/255.0, alpha: 1.0)
}
let icon: String
switch seismic.provider.uppercased() {
case "USGS": icon = "star"
case "SGC": icon = "star3"
case "CSN": icon = "star3f"
case "SSN": icon = "star4"
case "INPRES": icon = "star4r"
case "FUNVISIS": icon = "star6"
case "Ineter": icon = "triangle"
case "RSN": icon = "triangle2"
case "PHIVOLCS": icon = "triround_inner"
case "IGEPN": icon = "triround"
case "INGV": icon = "circle"
case "EMSC": icon = "dyamond"
case "IGP": icon = "dyamond_round"
case "JMA": icon = "esa"
case "GEONET": icon = "oct"
case "CSI": icon = "penta"
case "IGN": icon = "square"
case "UASD", "BDTIM", "NCS": icon = "thick_star"
case "RSPR": icon = "star6f"
case "UOA": icon = "triangle"
default: icon = ""
}
private class func imageColor(for seismic: EQNSisma) -> UIColor {
let magnitude = seismic.magnitude.doubleValue
if magnitude < 2.0 {
return UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) // white
} else if magnitude < 3.5 {
return UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0) // green
} else if magnitude < 4.5 {
return UIColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0) // yellow
} else if magnitude < 5.5 {
return UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0) // red
} else {
return UIColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 1.0) // magenta
}
return UIImage(named: "\(icon)_\(color)")
}
}

Some files were not shown because too many files have changed in this diff Show More