Compare commits

..

320 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
Andrea Busi bdfcb7a5c4 release: Increase version for release 2023-09-14 17:02:58 +02:00
Andrea Busi 40fcb4707f fix: Be sure to download updated data when country is changed 2023-09-14 17:02:17 +02:00
Andrea Busi 37f9a856b1 feat: Filter modify notification settings by default 2023-09-14 17:02:17 +02:00
Andrea Busi f42b9f1b53 refactor: Migrate some UserDefaults constants 2023-09-14 17:02:17 +02:00
Andrea Busi 4fd9966435 release: Update changelog 2023-09-14 17:02:17 +02:00
Andrea Busi df2c0a94a4 refactor: Remove unused import 2023-09-14 17:02:17 +02:00
Andrea Busi cdd1a8d875 refactor: Remove deprecated method, useless with new BGTask implementation 2023-09-14 17:02:17 +02:00
Andrea Busi 31f1cb5f35 refactor: Can enable/disable background position debug 2023-09-14 17:02:17 +02:00
Andrea Busi d426f15c8e refactor: Remove deprecated code 2023-09-14 17:02:17 +02:00
Andrea Busi 037a74061d feat: Perform WS request to upload user position 2023-09-14 17:02:17 +02:00
Andrea Busi ea6172226d feat: Add a debug screen for saved background positions 2023-09-11 15:52:27 +02:00
Andrea Busi ec94db29b9 feat: Add background task to get user location 2023-09-11 15:52:27 +02:00
Andrea Busi 91a9bce03c release: Update changelog 2023-08-16 12:03:09 +02:00
Andrea Busi f54f4a2312 refactor: Remove no longer needed available checks 2023-08-16 12:02:32 +02:00
Andrea Busi a959df7cd9 dependency: Update Pods & SPM 2023-08-16 12:02:18 +02:00
Andrea Busi e95a93ff2c feat: Increase target to iOS 13 2023-08-16 11:57:49 +02:00
Andrea Busi b7c1f7379d refactor: Migrate some user default constants 2023-08-16 11:54:52 +02:00
Andrea Busi 0f71e0fea9 refactor: Disable monitoring background logic 2023-08-14 16:33:54 +02:00
Andrea Busi 92de4c534c release: Increase version for release 2023-08-10 16:50:50 +02:00
Andrea Busi 3c237c5b18 docs: Add push payload for AR 2023-08-10 16:17:38 +02:00
Andrea Busi 16dc2410bc fix: Get user location if manager one is null 2023-08-10 15:58:59 +02:00
Andrea Busi 3c83cb97cb release: Increase version for release 2023-08-04 16:20:13 +02:00
Andrea Busi 4796e3d5a7 fix: Update AR string 2023-08-04 16:09:08 +02:00
Andrea Busi 56f53550da release: Increase version for release 2023-07-31 07:19:45 +02:00
Andrea Busi d52b980959 release: Increase version for release 2023-07-27 17:35:16 +02:00
Andrea Busi b9f87c130d fix: Add missing AR strings 2023-07-27 17:35:16 +02:00
Andrea Busi b7acbc70df feat: Add arabic language 2023-07-27 17:35:09 +02:00
Andrea Busi 093b6471e8 release: Increase version for release 2023-07-27 17:06:41 +02:00
Andrea Busi 8ca814561b fix: Improve colors in home 2023-07-27 17:06:33 +02:00
Andrea Busi 359667b659 release: Increase version for release 2023-07-24 17:31:39 +02:00
Andrea Busi 468659ee9f refactor: Align card colors with Android app 2023-07-24 17:30:28 +02:00
Andrea Busi ff50abd58a fix: Solve glitch in allerte table 2023-07-24 17:20:27 +02:00
Andrea Busi 4770578ae3 release: Increase version for release 2023-07-21 08:08:55 +02:00
Andrea Busi f0fe102901 feat: Use intensity color for alert card 2023-07-21 08:08:55 +02:00
Andrea Busi 9dacb33736 release: Increase version for release 2023-07-14 17:26:16 +02:00
Andrea Busi aed78e44cd feat: Add background animation in realtime alert 2023-07-14 16:59:58 +02:00
Andrea Busi e531088c86 feat: Add helper class for debug purpose 2023-07-14 16:17:26 +02:00
Andrea Busi 7e0112bf94 refactor: Use new extension method to evaluate difference between dates 2023-07-14 16:17:26 +02:00
Andrea Busi 5d12a86cfe refactor: Move alert expiration login inside new EQNRealtimePushNotification class 2023-07-14 16:17:26 +02:00
Andrea Busi 09e7786e65 feat: Handle realtime alert notification message based on intensity value 2023-07-14 16:17:26 +02:00
Andrea Busi 8671533e91 feat: Store lastLocation in appGroup instead of app user defaults 2023-07-14 16:17:15 +02:00
Andrea Busi 9bd94def0f refactor: Migrate UserDefaults key for app migration 2023-07-14 16:17:15 +02:00
Andrea Busi 29c325b7e2 refactor: Migrate UserDefaults key for user data informations 2023-07-14 16:17:15 +02:00
Andrea Busi 8366f2eabb refactor: Use new UserDefaults constants in NotificationService 2023-07-14 16:17:15 +02:00
Andrea Busi 7e26fee45b refactor: Migrate some UserDefaults from Macro to new constants 2023-07-14 16:17:15 +02:00
Andrea Busi 76d551b847 refactor: Create Swift constants file for UserDefaults 2023-07-14 16:17:15 +02:00
Andrea Busi 8f55553759 refactor: Print notification payload 2023-07-14 16:17:15 +02:00
Andrea Busi b44a0a2e27 refactor: Create a model to handle realtime push notification 2023-07-14 16:17:15 +02:00
Andrea Busi 2b98a4e292 dependency: Update Pods 2023-07-13 17:20:56 +02:00
Andrea Busi 44a27536ad refactor: Remove PurchasePro controller 2023-05-19 17:07:12 +02:00
Andrea Busi 76dcabdc5e fix: Use localized name for time zone in position cell 2023-05-19 17:07:03 +02:00
Andrea Busi 2405be895c refactor: Remove radius and intensity filters for real time notifications
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/61
2023-05-19 17:06:54 +02:00
Andrea Busi 89ca785864 refactor: Remove unused logic for Do not disturb notifications 2023-05-19 17:06:32 +02:00
Andrea Busi 60678d0839 refactor: Use proper way to manage plural in localized strings 2023-05-18 12:02:01 +02:00
Andrea Busi 88b36a501d refactor: Align strings with Android app and some reorder 2023-05-18 12:01:26 +02:00
Andrea Busi c5b4448830 refactor: Let Xcode reorganize storyboard 2023-05-18 12:00:43 +02:00
Andrea Busi 58b8960e21 refactor: Remove unused "Convert to pro" cell in allerts 2023-05-18 12:00:43 +02:00
Andrea Busi 49d210eca1 refactor: Remove unused strings 2023-05-12 10:46:14 +02:00
Andrea Busi 068e457297 release: Increase version for release 2023-03-28 18:10:53 +02:00
Andrea Busi 9796a40e0e release: Update changelg 2023-03-28 18:09:40 +02:00
Andrea Busi 01e8996572 refactor: Use new request parameters for network downloads endpoint 2023-03-28 18:09:40 +02:00
Andrea Busi 19c6b3d642 refactor: Move selected seismis networks management to EQNUserData 2023-03-28 18:09:40 +02:00
Andrea Busi 0c63a59f19 feat: Add UOA as seismic network 2023-03-28 18:09:40 +02:00
Andrea Busi f0c56584b8 chore: Update gitignore 2023-03-28 18:09:40 +02:00
Andrea Busi e9961af792 fix: Try to improve ObjC-Swift interoperability 2023-03-24 09:54:39 +01:00
Andrea Busi 806b4b67bf feat: Migrate to new network downloads endpoint and remove weather feature 2023-03-24 09:54:27 +01:00
Andrea Busi 851ece0a3b feat: Migrate smartphone download and subscription counter to new cached endpoints 2023-03-24 09:53:10 +01:00
Andrea Busi 0a76768f88 feat: Migrate user report endpoint to cache 2023-03-24 09:52:21 +01:00
Andrea Busi b15efe83e0 dependency: Rework Firebase configuration, as per official documentation 2023-03-21 18:06:18 +01:00
Andrea Busi d84dc8657a dependency: Update Shogun 2023-03-21 17:58:07 +01:00
Andrea Busi bac5e909bb dependency: Update Pods 2023-03-21 17:55:22 +01:00
Andrea Busi 15088b744f release: Increase version for release 2022-12-30 00:18:07 +01:00
Andrea Busi ac03a0cccb refactor: Update translations for tracking permission 2022-12-30 00:17:57 +01:00
Andrea Busi 76a26e3100 release: Increase version for release 2022-12-07 17:56:52 +01:00
Andrea Busi cac6ed67ac feat: Configure AppTracking and Facebook required flags 2022-12-07 09:26:37 +01:00
Andrea Busi 094c682dbd refactor: Reorganize Firebase configuration 2022-12-07 09:26:03 +01:00
Andrea Busi c44f46ca46 refactor: Reorganize push notification configuration 2022-12-07 09:25:38 +01:00
Andrea Busi 5e8c3d0796 release: Increase version fo release 2022-12-06 09:12:53 +01:00
Andrea Busi 61ce27ed4b feat: Enable Facebook SDK in app 2022-12-02 17:33:42 +01:00
Andrea Busi 3aea60e560 dependency: Add Facebook SDK 2022-12-02 15:18:18 +01:00
Andrea Busi 84b61fd7e2 release: Increase version for release 2022-11-28 22:21:51 +01:00
Andrea Busi a5a8c6f5c5 fix: Solve missing critical alerts after notification service migration 2022-11-28 10:33:30 +01:00
Andrea Busi 61587a0341 release: Increase version for release 2022-11-24 11:08:22 +01:00
Andrea Busi a56a04a4ad refactor: Recreate NotificationService (in Swift) to solve unpredictable behaviour
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/54
2022-11-24 11:08:22 +01:00
Andrea Busi 7173dc7031 dependency: Update Shogun 2022-11-24 10:52:39 +01:00
Andrea Busi 35dbdbab28 fix: Reduce space 2022-11-18 10:57:08 +01:00
Andrea Busi 14614267d4 feat: Add color in user report total 2022-11-18 10:50:24 +01:00
Andrea Busi 32833010ed dependency: Update Shogun 2022-11-18 10:50:08 +01:00
Andrea Busi ecc77e9f2b fix: Solve runtime warning 2022-11-18 08:34:41 +01:00
Andrea Busi e69747f133 refactor: Migrate preference to AppPreferences class 2022-11-18 08:33:35 +01:00
Andrea Busi 09685fd4a7 feat: Store user selection for compact/expanded view in suer report 2022-11-17 22:21:59 +01:00
Andrea Busi 3e122240dc refactor: Rework image creation 2022-11-17 22:13:18 +01:00
Andrea Busi 95a214403b release: Increase version for release 2022-11-17 15:44:07 +01:00
Andrea Busi a7db3c6fa1 docs: Add some example payloads 2022-11-17 15:42:32 +01:00
Andrea Busi 4805c79ed6 refactor: Merge icons and colors in a single asset, to solve issue with notification extensions
For some reasons, notification extension doesn't support multiple xcasset
2022-11-17 15:42:32 +01:00
Andrea Busi c6ec20e180 refactor: Migrate some extensions usage to Shogun 2022-11-17 15:42:32 +01:00
Andrea Busi f6dfd4a761 dependency: Add Shogun library 2022-11-17 15:42:32 +01:00
Andrea Busi 0212d0a15f fix: Minor UI tweaks in user reports map 2022-11-17 15:42:32 +01:00
Andrea Busi 3176bde5ed release: Increase version for release 2022-11-17 15:42:32 +01:00
Andrea Busi dfdbbd0ed4 feat: Update new user report data in notification extension 2022-11-17 15:42:00 +01:00
Andrea Busi e9986e0fe1 feat: Align user report map to Android app 2022-11-15 22:47:42 +01:00
Andrea Busi beb264f95e refactor: Move create snapshot to an extension method 2022-11-15 22:41:04 +01:00
Andrea Busi 217b5edbcf fix: Properly evaluate if ads is visible 2022-11-15 22:37:32 +01:00
Andrea Busi e13f95aa5d feat: Add color assets for user report 2022-11-15 12:28:54 +01:00
Andrea Busi fc5bdbcc92 fix: Solve issue with new registration logic 2022-11-15 12:28:54 +01:00
Andrea Busi d0ee637449 refactor: Remove send comment in manual report 2022-11-15 12:28:54 +01:00
Andrea Busi 66e9d7035e fix: Increase size for new report earthquake section buttons 2022-11-15 12:28:34 +01:00
Andrea Busi 7db48e381f release: Update changelog 2022-11-15 12:28:34 +01:00
Andrea Busi f9bdb84ad9 feat: Align 'last 24h user reports' card to Android 2022-11-11 15:16:16 +01:00
Andrea Busi bee18f6407 refactor: Use existing category for dictionary parsing 2022-11-11 15:16:16 +01:00
Andrea Busi 96f3a44db4 feat: Reset Firebase token to force a new server registration 2022-11-11 15:16:16 +01:00
Andrea Busi e394259f24 feat: New report earthquake section
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/51
2022-11-11 15:16:13 +01:00
Andrea Busi 3b53350969 refactor: Store lastLocation in EQNUserData 2022-11-11 15:15:47 +01:00
Andrea Busi f3c3c19e39 refactor: Store userId in EQNUserData 2022-11-11 15:15:47 +01:00
Andrea Busi 8404d72d8f refactor: Rework user registration to solve double registration
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/50
2022-11-11 15:15:40 +01:00
Andrea Busi a3b0499ed3 dependency: Update Pods 2022-10-28 12:21:29 +02:00
Andrea Busi d923b37fbd refactor: Update to Xcode 14.1 recommended settings 2022-10-28 12:12:26 +02:00
Andrea Busi 282803cf98 release: Increase version for release 2022-07-18 12:54:07 +02:00
Andrea Busi 039e7b82a1 fix: Don't show debug view when tap 5 times on Settings 2022-07-18 12:53:57 +02:00
Andrea Busi b098caf2ef chore: Update push payloads 2022-06-23 11:33:50 +02:00
Andrea Busi 73caf9647c release: Increase version for release 2022-06-22 16:16:24 +02:00
Andrea Busi 8700e200f9 chore: Update push payloads 2022-06-22 16:15:50 +02:00
Andrea Busi e99845ff1b fix: Add missing check for null alert 2022-06-22 09:27:47 +02:00
Andrea Busi a0161e8f4c release: Increase version for release 2022-06-17 14:42:47 +02:00
Andrea Busi 1b9944a7ca fix: Solve retain cycle and crashes 2022-06-17 14:42:47 +02:00
Andrea Busi 4c00e4ef6a refactor: Review time to show full screen view or card only 2022-06-17 14:42:47 +02:00
Andrea Busi 63592e6cfb refactor: Create a model for the realtime alert 2022-06-17 14:42:47 +02:00
Andrea Busi 2877dff23c refactor: Increase card interspace 2022-06-17 10:09:04 +02:00
Andrea Busi f2386a1abb refactor: Don't show countdown in expanded card 2022-06-17 10:09:04 +02:00
Andrea Busi 5e4a500f03 dependency: Update Pods 2022-06-17 10:09:04 +02:00
Andrea Busi 2b8f2db7c5 feat: Add new realtime alert screen 2022-06-17 10:09:04 +02:00
Andrea Busi 11d994696d release: Increase version for release 2022-06-09 22:20:56 +02:00
Andrea Busi cd6e20c1b2 fix: Solve issue with EMSC networks 2022-06-09 22:20:48 +02:00
Andrea Busi af5371571c release: Increase version for release 2022-06-03 17:44:54 +02:00
Andrea Busi 6291b22df0 fix: Add a delay before calling the completion, to let iOS remove already posted notification 2022-06-03 17:44:32 +02:00
Andrea Busi 2a7cfd3079 release: Increase version for release 2022-06-01 18:10:01 +02:00
Andrea Busi cae5fee992 feat: Remove same type posted notifications
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/48
2022-05-30 16:02:46 +02:00
Andrea Busi 53b8c0fab4 release: Increase version for release 2022-05-29 22:16:19 +02:00
Andrea Busi 8751d3c8f2 fix: Solve crazy behaviour with iOS 15 2022-05-29 22:14:06 +02:00
Andrea Busi 2d333f993b feat: Make slider with step values 2022-05-29 22:13:46 +02:00
Andrea Busi 22d421f1cc refactor: Add debug log 2022-05-26 21:51:30 +02:00
Andrea Busi cec8b39fc7 fix: Be sure to update push token in Firebase 2022-05-26 21:51:08 +02:00
Andrea Busi 5ceaa4a8be fix: Save user position after server registration 2022-05-26 18:06:37 +02:00
Andrea Busi 94bdb9dbe1 fix: Don't try to parse nil data 2022-05-26 18:06:27 +02:00
Andrea Busi ee1b762032 fix: Call callback for position endpoint 2022-05-26 18:06:13 +02:00
Andrea Busi 5a6c5a5cfc release: Increase version for release 2022-05-22 21:31:58 +02:00
Andrea Busi d55b2ec98f fix: Solve wrong value save in settings 2022-05-22 21:31:50 +02:00
Andrea Busi 761ebc1d17 release: Increase version for release 2022-05-22 11:54:06 +02:00
Andrea Busi dd5e8862ed fix: Don't reload entire table view in settings, to avoid strange behaviour with iOS 15 2022-05-22 11:53:23 +02:00
Andrea Busi ceca3ed50d fix: Remove wrong translatesAutoresizingMaskIntoConstraints for cell views 2022-05-22 11:28:07 +02:00
613 changed files with 11708 additions and 8100 deletions
+3
View File
@@ -1,3 +1,6 @@
# MacOS files
.DS_Store
# Exclude Pods
Sources/Pods
+91
View File
@@ -1,5 +1,96 @@
# 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
- Aggiunto invio posizione in background
## Versione 5.6
- Aggiunta lingua araba
## Versione 5.5
- Aggiornata integrazione Firebase
- Migrati alcuni endpooint a cache.earthquakenetwork.it
- Rimossa funzionalità meteo
- Aggiunta rete UOA
- Rimosse stringhe non utilizzate
- Rimosse sezioni non utilizzate (es PRO)
- Aggiunta gestione localizzata plurali
- Rimossi filtri intensità da notifiche in tempo reale
- Rivista gestione notifiche push allerte
- Migrate costanti in nuova struttura Swift
## Versione 5.4
- Aggiunto SDK Facebook
## Versione 5.3.2
- Corretto problema con notifiche critiche
## Versione 5.3.1
- Ricreato progetto NotificationService
- Correzioni minori
## Versione 5.3
- Rivista gestione registrazione utente
- Modifica in segnalazioni utente
- Allineata mappa segnalazioni utente ad Android
## Versione 5.2.1
- Disattivata schermata di debug
## Versione 5.2
- Nuova schermata per allerta in tempo reale
## Versione 5.1
### Build (91)
- Corretto problema con selezioni reti EMSC
### Build (90)
- Rimozione notifiche dello stesso tipo
## Versione 5.0.3
### Build (89)
- Corretto problema con ultima posizione non salvata in app
- Forzato aggiornamento Firebase
- Corretti metodi deprecati in salvataggio dati
- Corretti comportamento filtri con iOS 15
- Slider con valori a step
## Versione 5.0.2
### Build (88)
- Corretto errato salvataggio raggio sismi forti in allerte tempo reale
## Versione 5.0.1
### Build (87)
- Corretto problema con slider in impostazioni
## Versione 5.0
### Build (86)
+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"
}
@@ -0,0 +1,34 @@
{
"Simulator Target Bundle": "com.finazzi.distquake",
"aps": {
"alert": {
"loc-args": [
"2 km da Foligno"
],
"loc-key": "Sisma segnalato da utente a",
"title-loc-key": "Allerta sismica in tempo reale"
},
"category": "notifica_con_mappa",
"content-available": 1,
"mutable-content": 1,
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2022-06-22 19:12:00",
"delay": 4,
"detection_latitude": "45.64664",
"detection_longitude": "9.188540",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"magnitude": 20,
"latitude": "42.958",
"location": "2 km da Foligno",
"longitude": "12.702",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "manual",
"wave_speed": "4.7",
"critical": true
}
@@ -0,0 +1,34 @@
{
"Simulator Target Bundle": "com.finazzi.distquake",
"aps": {
"alert": {
"loc-args": [
"14 km Al Qunfudhah"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
},
"category": "notifica_con_mappa",
"content-available": 1,
"mutable-content": 1,
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2023-08-10 15:57:00",
"delay": 4,
"detection_latitude": "18.65",
"detection_longitude": "42.76",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "18.65",
"location": "14 km Al Qunfudhah",
"longitude": "42.76",
"peak": "1.2",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
}
+10 -9
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"15 χλμ από το Αίγιο"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,20 +14,21 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 21:34:50",
"datetime": "2022-06-23 10:10:00",
"delay": 4,
"detection_latitude": "37.983810",
"detection_longitude": "23.727539",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "37.683810",
"location": "150 km (Test)",
"longitude": "23.327539",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "38.19",
"location": "15 χλμ από το Αίγιο",
"longitude": "22.26",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
}
}
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"14 km from California City"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-16 12:41:20",
"datetime": "2022-06-23 07:55:00",
"delay": 4,
"detection_latitude": "37.3229978",
"detection_longitude": "-122.0321823",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "37.9229978",
"location": "150 km (Test)",
"longitude": "-121.0321823",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "35.15",
"location": "14 km from California City",
"longitude": "-117.78",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"5 km de Las Cujas"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 20:39:00",
"datetime": "2022-06-23 10:19:00",
"delay": 4,
"detection_latitude": "-34.603722",
"detection_longitude": "-58.381592",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "-34.103722",
"location": "150 km (Test)",
"longitude": "-58.781592",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "-32.57",
"location": "5 km de Las Cujas",
"longitude": "-71.46",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"15 km de Dieppe"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 21:37:40",
"datetime": "2022-06-23 10:07:00",
"delay": 4,
"detection_latitude": "48.856614",
"detection_longitude": "2.8522219",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "48.456614",
"location": "150 km (Test)",
"longitude": "2.3522219",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "49.77",
"location": "15 km de Dieppe",
"longitude": "1.05",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"5 km od Novog"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 21:39:50",
"datetime": "2022-06-23 10:02:00",
"delay": 4,
"detection_latitude": "45.813177",
"detection_longitude": "15.977048",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "45.413177",
"location": "150 km (Test)",
"longitude": "15.677048",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "45.09",
"location": "5 km od Novog",
"longitude": "14.87",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"35 km dari Sindanbarang"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 21:42:40",
"datetime": "2022-06-23 10:13:00",
"delay": 4,
"detection_latitude": "-2.548926",
"detection_longitude": "118.0148634",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "-2.948926",
"location": "150 km (Test)",
"longitude": "118.6148634",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "-7.38",
"location": "35 km dari Sindanbarang",
"longitude": "106.83",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"2 km da Foligno"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 20:32:20",
"datetime": "2022-06-22 19:12:00",
"delay": 4,
"detection_latitude": "45.64664",
"detection_longitude": "9.188540",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "45.164664",
"location": "150 km (Test)",
"longitude": "9.788540",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "42.958",
"location": "2 km da Foligno",
"longitude": "12.702",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"45 km de Puebla"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-16 12:39:40",
"datetime": "2022-06-23 10:16:00",
"delay": 4,
"detection_latitude": "19.432608",
"detection_longitude": "-99.133209",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "19.932608",
"location": "150 km (Test)",
"longitude": "-98.033209",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "18.72",
"location": "45 km de Puebla",
"longitude": "-97.90",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+10 -9
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"Bandırma'ya 25 km"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,20 +14,21 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 21:44:30",
"datetime": "2022-06-23 10:22:00",
"delay": 4,
"detection_latitude": "41.008238",
"detection_longitude": "28.978359",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "41.608238",
"location": "150 km (Test)",
"longitude": "28.678359",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "40.33",
"location": "Bandırma'ya 25 km",
"longitude": "27.66",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
}
}
@@ -41,6 +41,7 @@
[super viewDidLoad];
[self.mapView registerClass:[EQNCustomAnnotationView class] forAnnotationViewWithReuseIdentifier:EQNCustomAnnotationView.SingleLineIdentifier];
[self.mapView registerClass:[EQNCustomAnnotationView class] forAnnotationViewWithReuseIdentifier:EQNCustomAnnotationView.SmallIdentifier];
}
- (void)didReceiveNotification:(UNNotification *)notification
@@ -67,9 +68,8 @@
[self.mapView addAnnotation:annotation];
} else if ([userInfo[@"type"] isEqualToString:@"manual"]){
EQNMapAnnotationUserReport *annotation = [[EQNMapAnnotationUserReport alloc] initWithTitle:@""
coordinate:coordinate.coordinate
magnitude:[userInfo[@"magnitudo"] intValue]];
EQNMapAnnotationUserReport *annotation = [[EQNMapAnnotationUserReport alloc] initWithMagnitude:[userInfo[@"magnitude"] intValue]
coordinate:coordinate.coordinate];
[self.mapView addAnnotation:annotation];
}
@@ -94,7 +94,7 @@
NSTimeInterval difference = MAX([self.userSeismicTimestamp timeIntervalSinceDate:now], 0);
NSInteger seconds = (int)lround(difference);
self.waveLabel.text = [NSString stringWithFormat:NSLocalizedString(@"alert_wave", @""), seconds];
self.waveLabel.text = [NSString localizedStringWithFormat:NSLocalizedString(@"alert_wave", @""), seconds];
if (difference <= 0) {
// stop the countdown
@@ -118,9 +118,9 @@
} else if ([annotation isKindOfClass:[EQNMapAnnotationUserReport class]]) {
EQNMapAnnotationUserReport *report = (EQNMapAnnotationUserReport *)annotation;
EQNCustomAnnotationView *annotationView = (EQNCustomAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:EQNCustomAnnotationView.SingleLineIdentifier];
annotationView.image = report.image;
annotationView.title = report.title;
EQNCustomAnnotationView *annotationView = (EQNCustomAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:EQNCustomAnnotationView.SmallIdentifier];
annotationView.image = [report imageWithHeight:EQNCustomAnnotationView.SmallViewImageHeight];
annotationView.title = report.timeDifference;
return annotationView;
}
return nil;
@@ -1,4 +0,0 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

+1 -24
View File
@@ -2,35 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>EQNNotificationService</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>NotificationService</string>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>
@@ -1,13 +0,0 @@
//
// NotificationService.h
// EQNNotificationService
//
// Refactored by Andrea Busi
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UserNotifications/UserNotifications.h>
@interface NotificationService : UNNotificationServiceExtension
@end
@@ -1,176 +0,0 @@
//
// NotificationService.m
// EQNNotificationService
//
// Refactored by Andrea Busi
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "NotificationService.h"
#import "EQNAllertaSismica.h"
#import "Costanti.h"
@interface NotificationService ()
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@end
@implementation NotificationService
static NSString * const EQNSoundNotificationEQN = @"alert_sound.wav";
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent *_Nonnull))contentHandler
{
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// Configure the notification's payload.
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
content.title = [NSString localizedUserNotificationStringForKey:request.content.title arguments:nil];
content.body = [NSString localizedUserNotificationStringForKey:request.content.body arguments:nil];
self.bestAttemptContent.title = content.title;
self.bestAttemptContent.body = content.body;
// check for required informations
NSDictionary *userInfo = request.content.userInfo;
NSString *notificationType = [userInfo objectForKey:@"type"];
if (userInfo == nil || notificationType == nil) {
[self contentComplete];
return;
}
NSString *iconName = @"";
if ([notificationType isEqualToString:@"eqn"]) {
// check if user has enabled critical alerts
NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:EQNUserDefaultAppGroupSuite];
BOOL criticalAlertsEnabled = [sharedDefaults boolForKey:NOTIFICHE_ALLERA_SISMICA_CRITICAL_ALERTS];
// !!WORKAROUND
// this is a workaround to use critical alerts with legacy FCM api
// when the server implementation will be migrated to Firebase v1 APIs, this could be removed
BOOL isCritical = [[userInfo objectForKey:@"critical"] boolValue];
if (isCritical && criticalAlertsEnabled) {
self.bestAttemptContent.sound = [UNNotificationSound criticalSoundNamed:EQNSoundNotificationEQN withAudioVolume:1.0];
} else {
self.bestAttemptContent.sound = [UNNotificationSound soundNamed:EQNSoundNotificationEQN];
}
NSString *intensity = [userInfo objectForKey:@"intensity"];
switch ([intensity intValue]) {
case 0:
iconName = @"star_realtime_white.png";
break;
case 1:
iconName = @"star_realtime_lightblue.png";
break;
case 2:
iconName = @"star_realtime_blue.png";
break;
default:
break;
}
} else if ([notificationType isEqualToString:@"manual"]) {
NSString *intensity = [userInfo objectForKey:@"magnitude"];
switch ([intensity intValue]) {
case 0:
iconName = @"star_green.png";
break;
case 1:
iconName = @"star_yellow.png";
break;
case 2:
iconName = @"star_red.png";
break;
default:
break;
}
} else if ([notificationType isEqualToString:@"official"]) {
NSString *provaider = [userInfo objectForKey:@"provider"];
double intensity = [[userInfo objectForKey:@"magnitude"] doubleValue];
NSString *colore = @"";
if (intensity < 2.0) {
colore = @"_white";
} else if (intensity < 3.5) {
colore = @"_green";
} else if (intensity < 4.5) {
colore = @"_yellow";
} else if (intensity < 5.5) {
colore = @"_red";
} else {
colore = @"_purple";
}
if ([provaider isEqualToString:@"USGS"]) {
iconName = [NSString stringWithFormat:@"star%@.png", colore];
} else if ([provaider isEqualToString:@"SGC"]) {
iconName = [NSString stringWithFormat:@"star3%@.png", colore];
} else if ([provaider isEqualToString:@"CSN"]) {
iconName = [NSString stringWithFormat:@"star3f%@.png", colore];
} else if ([provaider isEqualToString:@"SSN"]) {
iconName = [NSString stringWithFormat:@"star4%@.png", colore];
} else if ([provaider isEqualToString:@"INPRES"]) {
iconName = [NSString stringWithFormat:@"star4r%@.png", colore];
} else if ([provaider isEqualToString:@"FUNVISIS"]) {
iconName = [NSString stringWithFormat:@"star6%@.png", colore];
} else if ([provaider isEqualToString:@"Ineter"]) {
iconName = [NSString stringWithFormat:@"triangle%@.png", colore];
} else if ([provaider isEqualToString:@"RSN"]) {
iconName = [NSString stringWithFormat:@"triangle2%@.png", colore];
} else if ([provaider isEqualToString:@"PHIVOLCS"]) {
iconName = [NSString stringWithFormat:@"triround_inner%@.png", colore];
} else if ([provaider isEqualToString:@"IGEPN"]) {
iconName = [NSString stringWithFormat:@"triround%@.png", colore];
} else if ([provaider isEqualToString:@"INGV"]) {
iconName = [NSString stringWithFormat:@"circle%@.png", colore];
} else if ([provaider isEqualToString:@"EMSC"]) {
iconName = [NSString stringWithFormat:@"dyamond%@.png", colore];
} else if ([provaider isEqualToString:@"IGP"]) {
iconName = [NSString stringWithFormat:@"dyamond_round%@.png", colore];
} else if ([provaider isEqualToString:@"JMA"]) {
iconName = [NSString stringWithFormat:@"esa%@.png", colore];
} else if ([provaider isEqualToString:@"GEONET"]) {
iconName = [NSString stringWithFormat:@"oct%@.png", colore];
} if ([provaider isEqualToString:@"CSI"]) {
iconName = [NSString stringWithFormat:@"penta%@.png", colore];
} else if ([provaider isEqualToString:@"IGN"]) {
iconName = [NSString stringWithFormat:@"square%@.png", colore];
} else if ([provaider isEqualToString:@"UASD"] || [provaider isEqualToString:@"BDTIM"] || [provaider isEqualToString:@"NCS"]) {
iconName = [NSString stringWithFormat:@"thick_star%@.png", colore];
} else if ([provaider isEqualToString:@"RSPR"]) {
iconName = [NSString stringWithFormat:@"star6f%@.png", colore];
}
}
if (![iconName isEqualToString:@""]) {
iconName = [iconName stringByReplacingOccurrencesOfString:@".png" withString:@""];
NSURL *imageUrl = [NSBundle.mainBundle URLForResource:iconName withExtension:@"png"];
NSError *attachmentError = nil;
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:iconName
URL:imageUrl
options:nil
error:&attachmentError];
if (!attachmentError) {
self.bestAttemptContent.attachments = @[attachment];
}
}
[self contentComplete];
}
- (void)serviceExtensionTimeWillExpire
{
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
[self contentComplete];
}
- (void)contentComplete
{
self.contentHandler(self.bestAttemptContent);
}
@end
@@ -0,0 +1,197 @@
//
// NotificationService.swift
// EQNNotificationService
//
// Created by Andrea Busi on 24/11/22.
// Copyright © 2022 Earthquake Network. All rights reserved.
//
import UserNotifications
import CoreLocation
import Shogun
class NotificationService: UNNotificationServiceExtension {
private static let EQNSoundNotification = UNNotificationSoundName("alert_sound.wav")
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
guard let bestAttemptContent = bestAttemptContent else {
print("[NotificationService] Unable to get notification content")
contentComplete()
return
}
let userInfo = request.content.userInfo
guard let notificationType = userInfo.string(forKey: "type") else {
print("[NotificationService] Unable to get notification type")
contentComplete()
return
}
var iconName = ""
switch notificationType.lowercased() {
case "eqn":
// check if user has enabled critical alerts
let criticalAlertsEnabled = UserDefaults.appGroup?.bool(forKey: UserDefaults.AllertaSismicaCriticalAlerts) ?? false
// !!WORKAROUND
// this is a workaround to use critical alerts with legacy FCM api
// when the server implementation will be migrated to Firebase v1 APIs, this could be removed
let isCritical = userInfo.integer(forKey: "critical", orDefault: 0) == 1
if isCritical && criticalAlertsEnabled {
bestAttemptContent.sound = UNNotificationSound.criticalSoundNamed(Self.EQNSoundNotification)
} else {
bestAttemptContent.sound = UNNotificationSound(named: Self.EQNSoundNotification)
}
let intensity = userInfo.integer(forKey: "intensity")
switch intensity {
case 0:
iconName = "star_realtime_white.png"
case 1:
iconName = "star_realtime_lightblue.png"
case 2:
iconName = "star_realtime_blue.png"
default:
break
}
// evaluate intensity and get proper string to display
guard let latitude = userInfo.double(forKey: "latitude"),
let longitude = userInfo.double(forKey: "longitude"),
let peak = userInfo.double(forKey: "peak") else {
print("[NotificationService] Unable to get base info for intensity calculation")
return
}
let location = CLLocation(latitude: latitude, longitude: longitude)
guard let distance = EQNUserData.shared.lastLocation?.distance(from: location) else {
print("[NotificationService] Unable to calculate distance or get last location")
return
}
let distanceKm = distance / 1_000
let intensita = peak * exp(-distanceKm/peak/250)
let stringSuffix: String
if intensita < 0.004 {
stringSuffix = "no_shaking"
} else if intensita < 0.30 {
stringSuffix = "mild"
} else if intensita < 0.70 {
stringSuffix = "moderate"
} else {
stringSuffix = "strong"
}
bestAttemptContent.body = "alert_intensity_\(stringSuffix)".localized
case "manual":
// there are 12 levels, so a customized icon doesn't make sense
// use a generic warning icon instead
iconName = "warning_yellow.png"
case "official":
// don't show any images
break
default:
break
}
// add the icon as notification attachment
if !iconName.isEmpty {
iconName = iconName.replacingOccurrences(of: ".png", with: "")
if let imageUrl = Bundle(for: NotificationService.self).url(forResource: iconName, withExtension: "png"),
let attachment = try? UNNotificationAttachment(identifier: iconName, url: imageUrl) {
bestAttemptContent.attachments = [ attachment ]
}
}
// remove same type posted notification
removeNotifications(for: notificationType) { [weak self] in
self?.contentComplete()
}
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
// MARK: - Private
private func contentComplete() {
if let bestAttemptContent {
contentHandler?(bestAttemptContent)
}
}
private func removeNotifications(
for type: String?,
completion: @escaping() -> Void
) {
guard let type = type else {
completion()
return
}
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.getDeliveredNotifications { notifications in
let sameTypeNotifications = notifications.filter { notification in
let payload = notification.request.content.userInfo
if let notificationType = payload["type"] as? String {
return notificationType == type
}
return false
}
let identifiers = sameTypeNotifications.map { $0.request.identifier }
notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers)
// !! Note: this is a known issue/bug
// we need to add a delay before invoking the completion, otherwise the notification will not be remved
// ref: https://stackoverflow.com/questions/53697279/why-are-notifications-not-removed-with-removedeliverednotifications
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
completion()
}
}
}
// MARK: - Helpers
private func manualIconName(for provider: String, color: String) -> String {
switch provider.uppercased() {
case "USGS": return "star\(color).png"
case "SGC": return "star3\(color).png"
case "CSN": return "star3f\(color).png"
case "SSN": return "star4\(color).png"
case "INPRES": return "star4r\(color).png"
case "FUNVISIS": return "star6\(color).png"
case "INETER": return "triangle\(color).png"
case "RSN": return "triangle2\(color).png"
case "PHIVOLCS": return "triround_inner\(color).png"
case "IGEPN": return "triround\(color).png"
case "INGV": return "circle\(color).png"
case "EMSC": return "dyamond\(color).png"
case "IGP": return "dyamond_round\(color).png"
case "JMA": return "esa\(color).png"
case "GEONET": return "oct\(color).png"
case "CSI": return "penta\(color).png"
case "IGN": return "square\(color).png"
case "UASD", "BDTIM", "NCS": return "thick_star\(color).png"
case "RSPR": return "star6f\(color).png"
case "UOA": return "triangle\(color).png"
default: return ""
}
}
}
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 = "1200"
LastUpgradeVersion = "1620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
@@ -74,6 +74,7 @@
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
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">
@@ -74,6 +74,7 @@
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
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>
+99 -58
View File
@@ -8,21 +8,19 @@
#import "AppDelegate.h"
#import "Costanti.h"
#import "ServerRequest.h"
#import "EQNGeneratoreURLServer.h"
#import "EQNUser.h"
#import "EQNAccelerometroManager.h"
#import "EQNManager.h"
#import "EQNUtility.h"
#import "EQNAllertaSismica.h"
#import "EQNNotificheSegnalazioniUtente.h"
#import "EQNNotificheReteSismiche.h"
#import "EQNMainTabBarController.h"
#import "NSDictionary+EQNExtensions.h"
@import Firebase;
@import FirebaseCrashlytics;
@import UserNotifications;
@import AppTrackingTransparency;
@import FirebaseCore;
@import FirebaseMessaging;
@import FirebaseCrashlytics;
@import GoogleMobileAds;
@import FBSDKCoreKit;
@interface AppDelegate () <FIRMessagingDelegate, UNUserNotificationCenterDelegate>
@property (strong, nonatomic) NSArray<id<EQNCommandProtocol>> *commands;
@@ -43,29 +41,25 @@
}];
#endif
[EQNUser defaultUser];
[EQNManager defaultManager];
[FIRApp configure];
[FIRMessaging messaging].delegate = self;
// add some generic logs for Crashlytics
NSString *language = [[NSLocale preferredLanguages] firstObject];
[[FIRCrashlytics crashlytics] setCustomValue:language forKey:@"lang"];
// execute commands
EQNStartupCommandsBuilder *builder = [[EQNStartupCommandsBuilder alloc] init];
self.commands = [builder build];
[builder executeWithCommands:self.commands];
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
[[UNUserNotificationCenter currentNotificationCenter]
requestAuthorizationWithOptions:authOptions
completionHandler:^(BOOL granted, NSError * _Nullable error) {
[self registraNotifica];
}];
[EQNUser defaultUser];
[EQNManager defaultManager];
[self configureFirebase];
[self configureFacebookSDKWithApplication:application andOptions:launchOptions];
// schedule background tasks
[BackgroundTaskManager.shared registerTasks];
[BackgroundTaskManager.shared scheduleUpdateServerPosition];
// add some generic logs for Crashlytics
NSString *language = [[NSLocale preferredLanguages] firstObject];
[[FIRCrashlytics crashlytics] setCustomValue:language forKey:@"lang"];
[self configurePushNotifications];
[application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
[application registerForRemoteNotifications];
return YES;
@@ -86,12 +80,14 @@
- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
[[EQNManager defaultManager] avviaManager];
[[EQNAccelerometroManager sharedInstance] startUpdatingLocationBackground];
NSUInteger counter = [[NSUserDefaults standardUserDefaults] integerForKey:EQNUserDefaultProDiscountOpenCounter];
// disabilitiamo logica calibrazione/monitoraggio perchè attualmente non utilizzata dal server
//[[EQNManager defaultManager] avviaManager];
//[[EQNAccelerometroManager sharedInstance] startUpdatingLocationBackground];
NSUInteger counter = [[NSUserDefaults standardUserDefaults] integerForKey:NSUserDefaults.UserDataProDiscountOpenCounter];
counter += 1;
[[NSUserDefaults standardUserDefaults] setInteger:counter forKey:EQNUserDefaultProDiscountOpenCounter];
[[NSUserDefaults standardUserDefaults] setInteger:counter forKey:NSUserDefaults.UserDataProDiscountOpenCounter];
}
@@ -124,7 +120,10 @@
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
NSString *token = [self stringWithDeviceToken:deviceToken];
[[NSUserDefaults standardUserDefaults] setObject:token forKey:EQNUserDefaultPushToken];
NSLog(@"[DEBUG] push token: %@", token);
[[NSUserDefaults standardUserDefaults] setObject:token forKey:NSUserDefaults.UserDataPushToken];
FIRMessaging.messaging.APNSToken = deviceToken;
}
- (NSString *)stringWithDeviceToken:(NSData *)deviceToken
@@ -146,47 +145,47 @@
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
// execute common logic and refresh the proper tab
NSDictionary *userInfo = notification.request.content.userInfo;
[self handlePushNotificationWithUserInfo:userInfo];
UNNotificationContent *content = notification.request.content;
[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.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler
{
NSLog(@"[DEBUG] push payload\n%@", [response.notification.request.content.userInfo eqn_jsonStringWithPrettyPrint:YES]);
// execute common logic and refresh the proper tab
NSDictionary *userInfo = response.notification.request.content.userInfo;
[self handlePushNotificationWithUserInfo:userInfo];
UNNotificationContent *content = response.notification.request.content;
[self handlePushNotificationWithNotificationContent:content];
completionHandler();
}
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSURL *url = [EQNGeneratoreURLServer urlPosizione];
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:url richiesta:EQNTipoChiamataPosizione success:^(id result) {
completionHandler(UIBackgroundFetchResultNewData);
} failure:^(NSError *error) {
completionHandler(UIBackgroundFetchResultFailed);
}];
}
#pragma mark - Private
- (void)handlePushNotificationWithUserInfo:(NSDictionary *)userInfo
- (void)handlePushNotificationWithNotificationContent:(UNNotificationContent *)content
{
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 ([userInfo[@"type"] isEqualToString:@"eqn"]) {
NSDate *dataRicezione = [NSDate date];
[[NSUserDefaults standardUserDefaults] setObject:dataRicezione forKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
[EQNUtility storeDictionary:userInfo toUserDefaultForKey:NOTIFICHE_RETE_SMARTPHONE_DIZIONARIO_NOTIFICA];
if ([type isEqualToString:@"eqn"]) {
[EQNRealtimePushNotification storeNotificationWithPayload:notification];
section = EQNTabBarSectionAllerte;
} else if([userInfo[@"type"] isEqualToString:@"manual"]) {
} else if([type isEqualToString:@"manual"]) {
section = EQNTabBarSectionSegnalazioni;
} else if([userInfo[@"type"] isEqualToString:@"official"]) {
} else if([type isEqualToString:@"official"]) {
[EQNOfficialPushNotification storeNotificationWithPayload:notification];
section = EQNTabBarSectionRetiSismiche;
}
@@ -194,19 +193,61 @@
[self.mainTabBarController selectSection:section];
}
#pragma mark - Configurations
- (void)configurePushNotifications
{
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError * _Nullable error) {
[self registraNotifica];
[self configureAppTracking];
}];
}
- (void)configureAppTracking
{
if (@available(iOS 14, *)) {
// add a delay otherwise the alert will not be displayed
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
if (status == ATTrackingManagerAuthorizationStatusAuthorized) {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = YES;
} else {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = NO;
}
}];
});
} else {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = YES;
}
}
- (void)configureFirebase
{
[FIRApp configure];
[FIRMessaging messaging].delegate = self;
}
- (void)configureFacebookSDKWithApplication:(UIApplication *)application andOptions:(NSDictionary *)launchOptions
{
[FBSDKApplicationDelegate.sharedInstance application:application didFinishLaunchingWithOptions:launchOptions];
FBSDKSettings.sharedSettings.isAdvertiserIDCollectionEnabled = YES;
}
#pragma mark - FIRMessagingDelegate
- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken
{
NSLog(@"[Firebase] fcmToken %@", fcmToken);
if (![[NSUserDefaults standardUserDefaults] objectForKey:EQNUserDefaultUserFirebaseToken]) {
if (EQNUserData.sharedData.isFirstStart) {
// save default values for notification settings
[EQNAllertaSismica saveDefaultValues];
[EQNNotificheSegnalazioniUtente saveDefaultValues];
[EQNNotificheReteSismiche saveDefaultValues];
[EQNSettingRealTimeAlert saveDefaultValues];
[EQNSettingUserReportNotification saveDefaultValues];
[EQNSettingSeismicNetworkNotification saveDefaultValues];
}
[EQNUser defaultUser].tokenUser = fcmToken;
[EQNUser.defaultUser registerUserIfNeededWithFirebaseToken:fcmToken];
}
@end
+113
View File
@@ -0,0 +1,113 @@
//
// Constants.swift
// Earthquake Network
//
// Created by Andrea Busi on 14/07/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
@objc
extension UserDefaults {
// UserDefaults condivisi con l'AppGroup
@objc(appGroupUserDefaults)
static let appGroup = UserDefaults(suiteName: "group.com.finazzi.distquake")
// Impostazioni della sezione `Allerta in tempo reale`
static let AllertaSismicaAbilitato = "NOTIFICHE_ALLERA_SISMICA_ABILITATO"
static let AllertaSismicaCriticalAlerts = "NOTIFICHE_ALLERA_SISMICA_CRITICAL_ALERTS"
static let AllertaSismicaSismiDaNotificare = "NOTIFICHE_ALLERA_SISMICA_SISMI_DA_NOTIFICARE"
static let AllertaSismicaRaggioSismiLievi = "NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_LIEVI"
static let AllertaSismicaRaggioSismiForti = "NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_FORTI"
// Impostazioni della sezione `Notifiche da reti sismiche`
static let NotificheRetiSismicheAbilitato = "NOTIFICHE_ATTIVA_RETI_SISMICHE"
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"
static let NotificheSegnalazioniUtenteDistanzaPosizione = "NOTIFICHE_SU_DISTANZA_POSIZIONE"
// Messaggio in segnalazione utente
static let UserReportMessage = "DATA_MESSAGE_EQN"
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
static let UserDataFirebaseToken = "EQNToken_User"
/// Server user ID dell'utente corrente
static let UserDataUserId = "EQNUSER_ID"
/// Reti sismiche selezionate
static let UserDataSelectedSeismicNetworks = "IMPOSTAZIONE_ENTI_RETI_SISMICHEI"
/// Token delle notifiche push
static let UserDataPushToken = "EQNetwork.PushToken"
/// Numero di aperture dell'app per sbloccare la versione Pro scontata
static let UserDataProDiscountOpenCounter = "CONTEGGIO_APERTURE_PER_SCONTO"
/// Prezzo scontato per la versione pro scaduto
static let UserDataProDiscountExpired = "PREZZO_SCONTATO_SCADUTO"
/// Se `true` visualizza il tempo nelle annotazioni della mappa segnalazioni utente
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"
}
extension UserDefaults {
/// Get a generic stored values
/// - Parameters:
/// - key: A key in the current users defaults database.
/// - defaultValue: Default value to return if the key is not found
/// - Returns: The object associated with the specified key, or `defaultValue` if the key was not found.
func object<T>(forKey key: String, or defaultValue: T) -> T {
if let value = UserDefaults.standard.object(forKey: key) as? T {
return value
}
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,9 +26,6 @@
@implementation AllerteViewController
static NSString * const SegueIdentifierProVersion = @"ShowProVersion";
static NSString * const SegueIdentifierPrioritySubscriptions = @"ShowPrioritySubscriptions";
/// Sections inside the app
typedef NS_ENUM(NSInteger, AllerteTableRow) {
AllerteTableRowLocationPermission = 0,
@@ -36,7 +33,6 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
AllerteTableRowAllertePassate,
AllerteTableRowReteSmartphone,
AllerteTableRowServizioPriorita,
AllerteTableRowVersionePro, // non più visualizzata con passaggio dell'app a pagamento
AllerteTableRowDatiPosizione
};
@@ -104,8 +100,21 @@ 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:)];
}
}
- (void)refreshUI
@@ -113,18 +122,22 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
[super refreshUI];
// `AllerteTableRowReteSmartphone` and `AllerteTableRowDatiPosizione` are hidden bu default, user can show them
BOOL showAllCards = [[NSUserDefaults standardUserDefaults] boolForKey:EQNUserDefaultKeyAlertsShowAllCards];
BOOL showAllCards = AppPreferences.shared.alertsShowAllCards;
self.expandeCollapseButton.image = showAllCards ? [UIImage imageNamed:@"navbar-icon-arrow-collapse"] : [UIImage imageNamed:@"navbar-icon-arrow-expand"];
NSDate *date = [[NSUserDefaults standardUserDefaults] objectForKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
if (date) {
if ([EQNUtility getDifferenceMinute:date] < TEMPO_VISUALIZZAZIONE_NOTIFICA)
self.isNotificaAttiva = YES;
else{
self.isNotificaAttiva = NO;
[[NSUserDefaults standardUserDefaults] removeObjectForKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
[[NSUserDefaults standardUserDefaults] synchronize];
// controlliamo se c'è una notifica in tempo reale da mostrare
EQNRealtimePushNotification *notification = [EQNRealtimePushNotification storedNotification];
if (notification) {
self.isNotificaAttiva = YES;
// mostriamo la schermata solo se il countdown non è a zero
if (![notification isCountdownExpired]) {
RealtimeAlertViewController *controller = [[RealtimeAlertViewController alloc] initWithNotification:notification];
controller.modalInPresentation = YES;
[self presentViewController:controller animated:YES completion:nil];
}
} else {
[self resetRealtimeAlert];
}
[self.tableItems removeAllObjects];
@@ -136,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)];
}
@@ -152,6 +165,12 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
[self.tableView reloadData];
}
- (void)resetRealtimeAlert
{
[EQNRealtimePushNotification removeStoredNotification];
self.isNotificaAttiva = NO;
}
#pragma mark - Actions
- (IBAction)refreshDataTapped:(id)sender
@@ -162,20 +181,21 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
- (IBAction)collapseExpandTapped:(id)sender
{
// toggle saved value
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL showAll = [defaults boolForKey:EQNUserDefaultKeyAlertsShowAllCards];
[defaults setBool:!showAll forKey:EQNUserDefaultKeyAlertsShowAllCards];
AppPreferences.shared.alertsShowAllCards = !AppPreferences.shared.alertsShowAllCards;
[self refreshUI];
}
- (IBAction)backgroundPositionDebugTapped:(id)sender
{
EQNBackgroundPositionDebugViewController *controller = [[EQNBackgroundPositionDebugViewController alloc] init];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:controller];
[self presentViewController:navController animated:YES completion:nil];
}
- (void)actionCloseNotification
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:NOTIFICHE_RETE_SMARTPHONE_DIZIONARIO_NOTIFICA];
[[NSUserDefaults standardUserDefaults] synchronize];
self.isNotificaAttiva = NO;
[self resetRealtimeAlert];
[self.tableView reloadData];
}
@@ -215,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)
@@ -252,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];
NSDictionary *info = [EQNUtility loadDictionaryFromUserDefaultsForKey:NOTIFICHE_RETE_SMARTPHONE_DIZIONARIO_NOTIFICA];
cell.notification = info;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
EQNRealtimePushNotification *notification = [EQNRealtimePushNotification storedNotification];
[cell updateWith:notification];
__weak AllerteViewController *weakSelf = self;
cell.onTapClose = ^{
@@ -278,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 = ^{
@@ -298,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];
};
@@ -307,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];
};
@@ -315,16 +340,13 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
} else if (tableRow == AllerteTableRowServizioPriorita) {
AlertsPriorityServiceTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PriorityCell" forIndexPath:indexPath];
cell.smartphoneNetwork = [EQNManager defaultManager].rete_smartphone;
return cell;
} else if (tableRow == AllerteTableRowVersionePro) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ProVersionCell" forIndexPath:indexPath];
[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;
}
@@ -337,12 +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 AllerteTableRowVersionePro:
[self performSegueWithIdentifier:SegueIdentifierProVersion 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,25 +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) {
if let sunrise = solar.sunrise {
sunriseTimeLabel.text = dateFormatter.string(from: sunrise) + " (\(TimeZone.current.identifier))"
}
if let sunset = solar.sunset {
sunsetTimeLabel.text = dateFormatter.string(from: sunset) + " (\(TimeZone.current.identifier))"
}
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,39 +8,81 @@
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 }
let formattedTime = EQNUtility.formattedString(forTimeDifference: smartphoneNetwork.lastSubscriptionDiff)
lastSubscriptionLabel.text = String(format: NSLocalizedString("inapp_adv_time", comment: ""), formattedTime)
lastSubscriptionLabel.text = subscriptionText(for: smartphoneNetwork.lastSubscriptionDiff)
}
private func subscriptionText(for time: Int) -> String {
var format = ""
var finalValue = time
// check for minutes, hours or days
if time < 60 {
format = NSLocalizedString("inapp_adv_minutes", comment: "")
} else if time < 1440 {
finalValue = time / 60
format = NSLocalizedString("inapp_adv_hours", comment: "")
} else {
finalValue = time / 1440
format = NSLocalizedString("inapp_adv_days", comment: "")
}
return String.localizedStringWithFormat(format, finalValue)
}
}
@@ -1,30 +0,0 @@
//
// AlertsProVersionTableViewCell.swift
// Earthquake Network
//
// Created by Andrea Busi on 05/04/21.
// Copyright © 2021 Earthquake Network. All rights reserved.
//
import UIKit
class AlertsProVersionTableViewCell: EQNBaseTableViewCell {
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var descriptionLabel: UILabel!
// MARK: - View Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
localizeUI()
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("network_pro", comment: "")
descriptionLabel.text = NSLocalizedString("network_topro", comment: "")
}
}
@@ -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?()
}
}
@@ -8,123 +8,198 @@
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: [String: Any]? {
didSet {
startCountdown()
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 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()
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 startCountdown() {
guard let notification = notification else { return }
// calculate the impact timestamp and start a timer for the countdown label
if let impactTimestamp = EQNUtility.calculateUserSeismicTimestamp(fromUserInfo: notification) {
self.impactTimestamp = impactTimestamp
countdownTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(countdownTimerFired(_:)), userInfo: nil, repeats: true)
countdownTimer?.fire()
}
}
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,
let aps = notification["aps"] as? [String: Any],
let alert = aps["alert"] as? [String: Any] else { return }
guard let notification = notification else { return }
backgroundColor = backgroundColor(for: notification.relativeIntensity())
notificationTitleLabel.text = notification.title
notificationIntensityLabel.text = notification.displayBody
notificationIntensityLabel.textColor = notification.relativeIntensityColor
let intensity = notification.eqn_intValue(for: "intensity") ?? 0
containerView.backgroundColor = color(for: intensity)
if let title = alert["loc-key"] as? String, let args = alert["loc-args"] as? [String], let arg = args.first {
notificationTitleLabel.text = String(format: NSLocalizedString(title, comment: ""), arg)
}
// get coordinate
var coordinate: CLLocation?
if let latitude = notification.eqn_doubleValue(for: "latitude"),
let longitude = notification.eqn_doubleValue(for: "longitude") {
if let date = notification.dateTime {
coordinate = CLLocation(latitude: latitude, longitude: longitude)
}
if let coordinate = coordinate,
let counter = notification["counter"],
let dateString = notification["datetime"] as? String,
let date = EQNUtility.getDateFrom(dateString) {
let distance = EQNUser.default().lastPosition?.distance(from: coordinate) ?? 0.0
let distance = EQNUser.default().lastPosition?.distance(from: notification.coordinate) ?? 0.0
let distanceRound = Int(round(distance / 1_000))
let difference = Int(NSDate().timeIntervalSince(date) / 60.0)
notificationDescriptionLabel.text = ""
+ NSLocalizedString("official_card_distance", comment: "") + " \(distanceRound) km"
+ " - " + EQNUtility.formattedString(forTimeDifference: difference)
+ "\n" + String(format: NSLocalizedString("map_number", comment: ""), "\(counter)")
}
if let coordinate = coordinate {
let span = MKCoordinateSpan(latitudeDelta: 10.5, longitudeDelta: 10.5)
let region = MKCoordinateRegion(center: coordinate.coordinate, span: span)
mapView.setCenter(coordinate.coordinate, animated: false)
mapView.setRegion(region, animated: true)
let annotation = EQNMapAnnotationPastquake(title: "", coordinate: coordinate.coordinate, intensity: intensity)
mapView.addAnnotation(annotation)
}
let span = MKCoordinateSpan(latitudeDelta: 10.5, longitudeDelta: 10.5)
let region = MKCoordinateRegion(center: notification.coordinate.coordinate, span: span)
mapView.setCenter(notification.coordinate.coordinate, animated: false)
mapView.setRegion(region, animated: true)
let annotation = EQNMapAnnotationPastquake(title: "", coordinate: notification.coordinate.coordinate, intensity: notification.intensity)
mapView.addAnnotation(annotation)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
@@ -138,44 +213,36 @@ class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMa
return annotationView
}
private func color(for intensity: Int) -> UIColor {
switch intensity {
case 0: return UIColor(red: 208.0/255.0, green: 234.0/255.0, blue: 201.0/255.0, alpha:1.0)
case 1: return UIColor(red: 254.0/255.0, green: 252.0/255.0, blue: 203.0/255.0, alpha:1.0)
case 2: return UIColor(red: 254.0/255.0, green: 186.0/255.0, blue: 186.0/255.0, alpha:1.0)
default: return UIColor.white
}
}
// MARK: - Actions
@objc private func countdownTimerFired(_ sender: Timer) {
guard let impactTimestamp = impactTimestamp else { return }
let now = Date()
let difference = lround(max(impactTimestamp.timeIntervalSince(now), 0))
waveTimeLabel.text = String(format: NSLocalizedString("alert_wave", comment: ""), difference)
if difference <= 0 {
// stop the countdown
countdownTimer?.invalidate()
countdownTimer = nil
}
}
@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: - Private
private func backgroundColor(for intensity: Double) -> UIColor {
switch intensity {
case _ where intensity < 0.004:
return AppTheme.Colors.cardBackgroundGray
case _ where intensity < 0.30:
return AppTheme.Colors.cardBackgroundGreen
case _ where intensity < 0.70:
return AppTheme.Colors.cardBackgroundYellow
default:
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?()
}
}
@@ -65,6 +65,8 @@ class PasquakesMapViewController: EQNBaseMapViewController {
}
override func didTapAnnotation(_ annotation: MKAnnotation) {
mapView.deselectAnnotation(annotation, animated: true)
guard let annotation = annotation as? EQNMapAnnotationPastquake, let pastquake = annotation.pastquake else {
return
}
@@ -96,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 {
@@ -0,0 +1,84 @@
//
// EQNBackgrounPositionDebugViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 14/08/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import UIKit
import CoreLocation
class EQNBackgroundPositionDebugViewController: UITableViewController {
private var positions = [EQNBackgroundPosition]()
private let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.timeStyle = .medium
formatter.dateStyle = .medium
return formatter
}()
private let helper = EQNBackgroundPositionDebugHelper()
// MARK: - Init
convenience init() {
self.init(style: .insetGrouped)
}
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
loadData()
}
private func configureUI() {
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60.0
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(onTapDeleteButton(_:)))
}
private func loadData() {
positions = helper.loadPosition()
tableView.reloadData()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
positions.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let position = positions[indexPath.row]
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "PositionCell")
cell.textLabel?.text = formatter.string(from: position.date)
cell.detailTextLabel?.text = String(format: "Lat: %.4f - Lon: %.4f", position.coordinate.latitude, position.coordinate.longitude)
var imageView: UIImageView?
switch position.request {
case .none:
imageView = nil
case .some(true):
imageView = .init(image: .init(systemName: "checkmark.circle.fill"))
imageView?.tintColor = AppTheme.Colors.green
case .some(false):
imageView = .init(image: .init(systemName: "x.circle.fill"))
imageView?.tintColor = AppTheme.Colors.red
}
cell.accessoryView = imageView
return cell
}
// MARK: - Actions
@objc private func onTapDeleteButton(_ sender: UIBarButtonItem) {
helper.resetPositions()
loadData()
}
}
@@ -57,8 +57,8 @@ class EQNDebugViewController: UIViewController {
}
private func loadDebugData() {
let firebaseToken = UserDefaults.standard.string(forKey: EQNUserDefaultUserFirebaseToken) ?? ""
let pushToken = UserDefaults.standard.string(forKey: EQNUserDefaultPushToken) ?? ""
let firebaseToken = UserDefaults.standard.string(forKey: UserDefaults.UserDataFirebaseToken) ?? ""
let pushToken = UserDefaults.standard.string(forKey: UserDefaults.UserDataPushToken) ?? ""
let text =
"""
@@ -29,7 +29,7 @@
[[EQNManager defaultManager] avviaManager];
[[EQNAccelerometroManager sharedInstance] startUpdatingLocationBackground];
self.testo = [NSString stringWithFormat:@" LOG ID UTENTE %@\n\nTOKEN FIREBASE:\n%@\n\n", [EQNUser defaultUser].user_ID, [EQNUser defaultUser].tokenUser];
self.testo = [NSString stringWithFormat:@" LOG ID UTENTE %@\n\nTOKEN FIREBASE:\n%@\n\n", [EQNUser defaultUser].user_ID, EQNUserData.sharedData.firebaseToken];
self.logView.text = self.testo;
}
@@ -21,7 +21,11 @@
- (BOOL)isBannerVisible
{
#if ADS_ENABLED
return ![EQNPurchaseUtility isProVersionEnabled];
#else
return NO;
#endif
}
#pragma mark - View Lifecycle
@@ -88,12 +92,9 @@
}
// Determine the view width to use for the ad width.
CGRect frame = self.view.frame;
// Here safe area is taken into account, hence the view frame is used after
// the view has been laid out.
if (@available(iOS 11.0, *)) {
frame = UIEdgeInsetsInsetRect(self.view.frame, self.view.safeAreaInsets);
}
CGRect frame = UIEdgeInsetsInsetRect(self.view.frame, self.view.safeAreaInsets);
CGFloat viewWidth = frame.size.width;
// Step 3 - Get Adaptive GADAdSize and set the ad view.
@@ -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
@@ -63,9 +77,9 @@ static NSString * const SegueIdentifierLogs = @"ShowLogs";
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"retry", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
// retry server registration
[[EQNUser defaultUser] verificaRegistrazione];
[[EQNUser defaultUser] retryUserRegistration];
}]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"options_cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"status_cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
});
}
@@ -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;
@@ -136,7 +150,7 @@ static NSString * const SegueIdentifierLogs = @"ShowLogs";
}
// tap 5 times on "Settings" to open debug view
if ([controller isKindOfClass:[SettingsViewController class]]) {
if ([controller isKindOfClass:[SettingsViewController class]] && EQNEnableDebugView) {
self.debugTapCounter += 1;
if (self.debugTapCounter == 5) {
self.debugTapCounter = 0;
@@ -1,181 +0,0 @@
//
// PurchaseProVersionViewController.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 PurchaseProVersionViewController: UIViewController {
@IBOutlet private weak var containerView: UIView!
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var subtitleLabel: UILabel!
@IBOutlet private weak var discountTextLabel: UILabel!
@IBOutlet private weak var descriptionTextLabel: UILabel!
@IBOutlet private weak var openPrivacyButton: UIButton!
@IBOutlet private weak var openTermsButton: UIButton!
@IBOutlet private weak var payingLabel: UILabel!
@IBOutlet private weak var priceLabel: UILabel!
@IBOutlet private weak var purchaseButton: UIButton!
// MARK: - Internal
private var products = [SKProduct]()
private var proProduct: SKProduct?
private var restoreTapped = false
/// Time remaining (in hours) for discounted price. If zero, no discount available
private var discountTimeRemaining: Int = 0
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(handlePurchaseNotification(_:)),
name: .EQNInAppPurchaseDidComplete,
object: nil)
configureUI()
loadProducts()
checkDiscountPrice()
}
// MARK: - Private
private func configureUI() {
let restoreButton = UIBarButtonItem(title: NSLocalizedString("purchase_pro_restore", comment: ""),
style: .plain,
target: self,
action: #selector(restoreTapped(_:)))
navigationItem.rightBarButtonItem = restoreButton
purchaseButton.isEnabled = false
titleLabel.text = NSLocalizedString("network_pro", comment: "")
subtitleLabel.text = NSLocalizedString("network_pro_subtitle", comment: "")
descriptionTextLabel.text = NSLocalizedString("purchase_pro_description", comment: "")
discountTextLabel.text = NSLocalizedString("purchase_pro_discount", comment: "")
discountTextLabel.isHidden = true
openPrivacyButton.setTitle(NSLocalizedString("network_pro_privacy_disclaimer", comment: ""), for: .normal)
openTermsButton.setTitle(NSLocalizedString("network_pro_terms_conditions", comment: ""), for: .normal)
payingLabel.text = NSLocalizedString("network_pro_paying", comment: "")
purchaseButton.setTitle(NSLocalizedString("network_pro_convert", comment: "").uppercased(), for: .normal)
containerView.eqn_applyShadowAndRoundedCorners()
}
private func updateUI() {
// search for the Pro product
let isDiscountEnabled = discountTimeRemaining > 0
let identifier = isDiscountEnabled ? VersioneProProducts.Identifier.ProVersionDiscounted : VersioneProProducts.Identifier.ProVersionFullPrice
guard let proProduct = products.first(where: { $0.productIdentifier.lowercased() == identifier.lowercased() }) else {
return
}
self.proProduct = proProduct
priceFormatter.locale = proProduct.priceLocale
if isDiscountEnabled {
discountTextLabel.isHidden = false
let string = NSLocalizedString("purchase_pro_discount", comment: "")
discountTextLabel.text = String(format: string, discountTimeRemaining)
}
priceLabel.text = priceFormatter.string(from: proProduct.price)
purchaseButton.isEnabled = true
if UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.ProVersionFullPrice) ||
UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.ProVersionDiscounted) ||
UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.Subscription10kYearly) ||
UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.Subscription10kYearlyDiscounted) ||
UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.Subscription100kYearly) ||
UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.Subscription100kYearlyDiscounted) {
purchaseButton.isEnabled = false
priceLabel.text = "-"
}
}
private func loadProducts() {
VersioneProProducts.store.requestProducts { [weak self] success, products in
guard let self = self, let products = products, success == true else { return }
self.products = products
self.updateUI()
}
}
private func checkDiscountPrice() {
EQNPurchaseUtility.offerTimeRemaining { (timeRemaining) in
DispatchQueue.main.async {
self.discountTimeRemaining = timeRemaining
self.updateUI()
}
}
}
// MARK: - Actions
@objc func restoreTapped(_ sender: Any) {
restoreTapped = true
VersioneProProducts.store.restorePurchases()
}
@IBAction func purchaseTapped(_ sender: UIButton) {
guard let product = proProduct else { return }
VersioneProProducts.store.buyProduct(product)
}
@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)
}
}
// MARK: - Notifications
@objc func handlePurchaseNotification(_ notification: Notification) {
guard let productId = notification.object as? String,
products.contains(where: { $0.productIdentifier == productId }) else {
print("[PurchasePro] Unable to find the product")
return
}
if restoreTapped {
restoreTapped = false
let alert = UIAlertController(title: NSLocalizedString("purchase_pro_restore_alert_title", comment: ""),
message: NSLocalizedString("purchase_pro_restore_alert_message", comment: ""),
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "ok", style: .default) { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
})
present(alert, animated: true)
} else {
navigationController?.popViewController(animated: true)
}
}
// MARK: - Helper
private var priceFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.formatterBehavior = .behavior10_4
formatter.numberStyle = .currency
return formatter
}()
}
@@ -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 {
@@ -8,39 +8,31 @@
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
@@ -48,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() {
@@ -63,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() {
@@ -89,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()
}
}
@@ -170,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)
}
@@ -189,7 +157,7 @@ class SubscriptionsViewController: UITableViewController {
// MARK: - Notifications
@objc func fail(_ notification: Notification){
VersioneProProducts.store.loadPurchase()
EQNInAppProducts.store.loadPurchase()
}
@objc func handlePurchaseNotification(_ notification: Notification) {
@@ -208,7 +176,7 @@ class SubscriptionsViewController: UITableViewController {
present(alert, animated: true, completion: nil)
}
VersioneProProducts.store.loadPurchase()
EQNInAppProducts.store.loadPurchase()
loadData()
}
@@ -227,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 {
@@ -248,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"
}
}
@@ -0,0 +1,226 @@
//
// RealtimeAlertView.swift
// Earthquake Network
//
// Created by Andrea Busi on 15/06/22.
// Copyright © 2022 Earthquake Network. All rights reserved.
//
import UIKit
import MapKit
class RealtimeAlertContainerView: UIView {
lazy var alertView: RealtimeAlertView = {
let view = RealtimeAlertView()
view.translatesAutoresizingMaskIntoConstraints = false
view.eqn_applyRoundedCorners()
view.clipsToBounds = true
return view
}()
var animationColor: UIColor = .white
// MARK: - Init
convenience init() {
self.init(frame: .zero)
configureUI()
}
// MARK: - Private
private func configureUI() {
backgroundColor = .white
addSubview(alertView)
let margin: CGFloat = 24
alertView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: margin).isActive = true
alertView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -margin).isActive = true
alertView.topAnchor.constraint(equalTo: topAnchor, constant: margin).isActive = true
alertView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -margin).isActive = true
}
// MARK: - Public
func startBackgroundAnimation() {
let animation = CABasicAnimation(keyPath: "backgroundColor")
animation.fromValue = UIColor.white.cgColor
animation.toValue = animationColor.cgColor
animation.duration = 2.0
animation.beginTime = CACurrentMediaTime() + 1
animation.autoreverses = true
animation.repeatCount = .infinity
layer.add(animation, forKey: "backgroundColor")
}
}
class RealtimeAlertView: UIView {
let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .title2)
label.text = NSLocalizedString("app_name", comment: "")
return label
}()
let descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
let waveTimeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
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
}()
let intensityLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .largeTitle)
label.textColor = AppTheme.Colors.red
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
lazy var mapView: MKMapView = {
let map = MKMapView()
map.translatesAutoresizingMaskIntoConstraints = false
map.delegate = self
map.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
map.showsUserLocation = true
return map
}()
let closeButton: EQNBlurredCloseButton = {
let button = EQNBlurredCloseButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(.darkGray, for: .normal)
return button
}()
// MARK: - Init
convenience init() {
self.init(frame: .zero)
configureUI()
}
// MARK: - Private
private func configureUI() {
backgroundColor = AppTheme.Colors.lightGray
addSubview(closeButton)
addSubview(titleLabel)
addSubview(descriptionLabel)
addSubview(mapView)
closeButton.addDefaultConstraint(to: self)
titleLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -10.0).isActive = true
titleLabel.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: 10.0).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor).isActive = true
descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20.0).isActive = true
let stackView = UIStackView(arrangedSubviews: [waveTimeLabel, intensityLabel])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.spacing = 20.0
addSubview(stackView)
stackView.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 20.0).isActive = true
mapView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
mapView.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 20.0).isActive = true
mapView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
// MARK: - Public
func addMapCircle(
center: CLLocationCoordinate2D,
radius: CLLocationDistance,
overlayId: String
) {
// remove any other existing overlays
let overlays = mapView.overlays.filter { $0.title == overlayId }
mapView.removeOverlays(overlays)
// add new overlay
let circle = MKCircle(center: center, radius: radius)
circle.title = overlayId
mapView.addOverlay(circle)
}
func addMapLine(
coordinates: [CLLocationCoordinate2D]
) {
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
mapView.addOverlay(polyline)
}
func addMapAnnotation(
title: String = "",
center: CLLocationCoordinate2D,
intensity: Int
) {
let annotation = EQNMapAnnotationPastquake(title: title, coordinate: center, intensity: intensity)
mapView.addAnnotation(annotation)
}
}
extension RealtimeAlertView: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
switch overlay {
case let circle as MKCircle:
let circleRenderer = MKCircleRenderer(overlay: circle)
circleRenderer.strokeColor = AppTheme.Colors.red
circleRenderer.fillColor = AppTheme.Colors.red.withAlphaComponent(0.2)
circleRenderer.lineWidth = 3.0
return circleRenderer
case let polyline as MKPolyline:
let polylineRenderer = MKPolylineRenderer(polyline: polyline)
polylineRenderer.strokeColor = .blue
polylineRenderer.lineWidth = 2.0
return polylineRenderer
default:
return MKOverlayRenderer(overlay: overlay)
}
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? EQNMapAnnotationPastquake else {
return nil
}
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: EQNCustomAnnotationView.SingleLineIdentifier, for: annotation) as! EQNCustomAnnotationView
annotationView.image = annotation.image
annotationView.title = annotation.title
return annotationView
}
}
@@ -0,0 +1,196 @@
//
// RealtimeAlertViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 15/06/22.
// Copyright © 2022 Earthquake Network. All rights reserved.
//
import UIKit
import MapKit
class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
@objc var onClose: () -> Void = {}
// MARK: - Internal
private let containerView = RealtimeAlertContainerView()
private var notificationView: RealtimeAlertView {
containerView.alertView
}
/// Alert to display
private let realtimeAlert: EQNRealtimePushNotification
/// Timer to constantly update countdown label
private var countdownTimer: Timer?
/// Refresh time for wave animation
private let waveAnimationRefreshRate = 0.1
/// Current radius of the wave animation on the map
private var waveAnimationCurrentRadius: CLLocationDistance = 0
private var waveAnimationVelocity: Double = 1_000
/// Timer to simulate animation for the wave
private var waveAnimationTimer: Timer?
// MARK: - Init
@objc
init(notification: EQNRealtimePushNotification) {
self.realtimeAlert = notification
super.init(nibName: nil, bundle: nil)
self.waveAnimationCurrentRadius = currentWavePosition()
self.waveAnimationVelocity = evaluateWaveAnimationVelocity()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
// importante togliere il delegato, altrimenti causa crash
notificationView.mapView.delegate = nil
}
// MARK: - View Lifecycle
override func loadView() {
view = containerView
}
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
updateUI()
startCountdown()
startWaveAnimation()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
containerView.startBackgroundAnimation()
}
// MARK: - Private
private func configureUI() {
notificationView.closeButton.addTarget(self, action: #selector(onTapClose(_:)), for: .touchUpInside)
// configure color for animation
containerView.animationColor = realtimeAlert.relativeIntensityColor
}
private func updateUI() {
notificationView.descriptionLabel.text = realtimeAlert.title
// update title with distance from earthquake
let distanceRound = Int(round(realtimeAlert.distanceFromUser() / 1_000))
notificationView.descriptionLabel.text = (notificationView.descriptionLabel.text ?? "")
+ ".\n"
+ String(format: NSLocalizedString("official_distance", comment: ""), distanceRound)
notificationView.intensityLabel.text = realtimeAlert.displayBody
notificationView.intensityLabel.textColor = realtimeAlert.relativeIntensityColor
// center map on the earthquake coordinate
let span = MKCoordinateSpan(latitudeDelta: 10.5, longitudeDelta: 10.5)
let region = MKCoordinateRegion(center: realtimeAlert.coordinate.coordinate, span: span)
notificationView.mapView.setCenter(realtimeAlert.coordinate.coordinate, animated: false)
notificationView.mapView.setRegion(region, animated: true)
// aggiungiamo annotation con epicentro sisma
notificationView.addMapAnnotation(center: realtimeAlert.coordinate.coordinate, intensity: realtimeAlert.intensity)
// simuliamo animazione dell'onda sismica
notificationView.addMapCircle(center: realtimeAlert.coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation")
// aggiungiamo un segmento tra la posizione del sisma e quella dell'utente
if let lastPosition = EQNUser.default().lastPosition {
notificationView.addMapLine(coordinates: [realtimeAlert.coordinate.coordinate, lastPosition.coordinate])
}
}
private func startCountdown() {
// show countdown only if time is less than 300 seconds
if realtimeAlert.currentCountdown() < 300 {
// start a timer for the countdown label
notificationView.waveTimeLabel.isHidden = false
countdownTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(countdownTimerFired(_:)), userInfo: nil, repeats: true)
countdownTimer?.fire()
}
}
private func startWaveAnimation() {
waveAnimationTimer = Timer.scheduledTimer(timeInterval: waveAnimationRefreshRate, target: self, selector: #selector(mapWaveAnimationFired(_:)), userInfo: nil, repeats: true)
waveAnimationTimer?.fire()
}
// MARK: - Action
@objc private func onTapClose(_ sender: UIButton) {
// invalidiamo i timer, altri
countdownTimer?.invalidate()
countdownTimer = nil
waveAnimationTimer?.invalidate()
waveAnimationTimer = nil
onClose()
dismiss(animated: true)
}
// MARK: - Timer
@objc private func countdownTimerFired(_ sender: Timer) {
let countdown = realtimeAlert.currentCountdown()
notificationView.waveTimeLabel.text = String.localizedStringWithFormat(NSLocalizedString("alert_wave", comment: ""), countdown)
notificationView.waveTimeLabel.textColor = waveTimeTextColor(for: countdown)
if countdown <= 0 {
// stop the countdown
countdownTimer?.invalidate()
countdownTimer = nil
}
}
@objc private func mapWaveAnimationFired(_ sender: Timer) {
waveAnimationCurrentRadius += waveAnimationVelocity
notificationView.addMapCircle(center: realtimeAlert.coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation")
}
// MARK: - Helpers
/// Evaluate current position for the wave
/// Used to define initial position for the wave circle
/// - Returns: Distance of the wave from the original earthquake point
private func currentWavePosition() -> Double {
// distanza tra utente e terremoto
let distance = realtimeAlert.distanceFromUser()
// calcoliamo la distanza rimanente da mostrare, perchè la schermata potrebbe anche essere aperta in ritardo
let remainingDistance = realtimeAlert.waveSpeed * Double(realtimeAlert.currentCountdown())
return distance - remainingDistance
}
/// Evaluate wave velocity based on push notification data
/// - Returns: Wave velocity, used for animation
private func evaluateWaveAnimationVelocity() -> Double {
let velocity = realtimeAlert.waveSpeed
return velocity * waveAnimationRefreshRate
}
/// Returns the text color based on impact countdown
private func waveTimeTextColor(for countdown: Int) -> UIColor {
switch countdown {
case _ where countdown > 15:
return UIColor(red: 255.0/255.0, green: 140.0/255.0, blue: 0.0, alpha: 1.0)
case _ where countdown > 5:
return UIColor(red: 255.0/255.0, green: 100.0/255.0, blue: 0.0, alpha: 1.0)
default:
return UIColor(red: 255.0/255.0, green: 0.0/255.0, blue: 0.0, alpha: 1.0)
}
}
}
@@ -7,51 +7,123 @@
//
import UIKit
import Shogun
class SegnalazioniLast24HoursCell: EQNBaseTableViewCell {
@objc
class SegnalazioniLast24HoursCell: EQNBaseContainerTableViewCell {
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var mildReportsLabel: UILabel!
@IBOutlet private weak var strongReportsLabel: UILabel!
@IBOutlet private weak var veryStrongReportsLabel: UILabel!
@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!
@objc var onTapTwitter: (() -> Void)?
@objc var onTapMap: (() -> Void)?
@objc var onTapTelegram: (() -> Void)?
// MARK: - View Lifecycle
override var isHeaderVisible: Bool { false }
override func awakeFromNib() {
super.awakeFromNib()
// 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
}()
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: - Internal
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("main_map", comment: "")
mapButton.setTitle(NSLocalizedString("official_button_map", comment: ""), for: .normal)
override func updateUI() {
super.updateUI()
reportsDescriptionLabel.text = NSLocalizedString("main_map", comment: "")
}
// 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 }
self.mildReportsLabel.text = "\(smartphoneNetwork.manualGreen)"
self.strongReportsLabel.text = "\(smartphoneNetwork.manualYellow)"
self.veryStrongReportsLabel.text = "\(smartphoneNetwork.manualRed)"
let reports = smartphoneNetwork.manual
self.reportsLabel.text = "\(reports)"
if reports < 100 {
self.reportsLabel.textColor = UIColor(hex6: 0x01b400)
} else if reports < 1000 {
self.reportsLabel.textColor = UIColor(hex6: 0xe8da00)
} else {
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?()
}
}
@@ -8,6 +8,7 @@
import Foundation
import MapKit
import Shogun
class SegnalazioniMapViewController: EQNBaseMapViewController {
@@ -16,11 +17,46 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
let circle: MKCircle
}
override var isCloseButtonVisible: Bool {
false
}
private let appPreferences = AppPreferences.shared
/// Contains circles and related colors to draw overlays on the map
private var mapCircles = [MapCircle]()
/// Reports currently showned on the map
private var filteredReports = [EQNSegnalazione]()
// MARK: - UI
private lazy var magnitudeLegendView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.distribution = .fillEqually
[20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120].forEach { magnitude in
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = (magnitude / 10).romanNumber()
label.backgroundColor = UIColor(named: "Mercalli \(magnitude)")
label.textAlignment = .center
label.font = .preferredFont(forTextStyle: .callout)
label.textColor = magnitude >= 100 ? .white : .black
stackView.addArrangedSubview(label)
}
view.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
return view
}()
// MARK: - View Lifecycle
override func viewDidLoad() {
@@ -29,8 +65,26 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
// MARK: - Public
override func extraUI() {
view.addSubview(magnitudeLegendView)
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
}
override func configureUI() {
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTapCloseButton(_:)))
navigationItem.rightBarButtonItems = [
UIBarButtonItem(image: UIImage(named: "navbar-icon-screenshot"), style: .plain, target: self, action: #selector(onTapScreenshotButton(_:))),
UIBarButtonItem(image: UIImage(named: "navbar-icon-pin-arrow"), style: .plain, target: self, action: #selector(onTapMapDetailStyleButton(_:)))
]
}
override func registerMapAnnotationViews() {
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SmallIdentifier)
}
override func loadDataSource() {
@@ -66,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
@@ -96,27 +150,28 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
}
}
override func didTapAnnotation(_ annotation: MKAnnotation) {
guard let annotation = annotation as? EQNMapAnnotationUserReport, let report = annotation.report else {
return
// MARK: - Actions
@objc private func onTapCloseButton(_ sender: Any) {
dismiss(animated: true)
}
@objc private func onTapMapDetailStyleButton(_ sender: Any) {
appPreferences.userReportExpandedView.toggle()
reloadMap()
}
@objc private func onTapScreenshotButton(_ sender: Any) {
let screenshot = createSnapshot {
// nascondiamo la legenda
magnitudeLegendView.isHidden = true
} restore: {
// ri-visualizziamo la legenda
magnitudeLegendView.isHidden = false
}
let difference = Int(Date().timeIntervalSince(report.date) / 60.0)
let title = EQNUtility.formattedString(forTimeDifference: difference) + " - \(report.intensity.description)"
var message = ""
+ "🏢 " + report.address
+ "\n" + EQNUtility.formattedDate(from: report.date) + " \(NSLocalizedString("share_yourtime", comment: ""))"
if !report.message.isEmpty {
message += "\n💬 \(report.message)"
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("main_share", comment: ""), style: .default, handler: { [unowned self] _ in
self.openShareActivity(for: report)
}))
alert.addAction(UIAlertAction(title: NSLocalizedString("official_close", comment: ""), style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
let controller = UIActivityViewController(activityItems: [screenshot], applicationActivities: [])
present(controller, animated: true)
}
// MARK: - Private
@@ -125,17 +180,17 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
let vector_latitude = reports.map { $0.coordinate.coordinate.latitude }
let vector_longitude = reports.map { $0.coordinate.coordinate.longitude }
let vector_date = reports.map { $0.date }
let vector_state = reports.map { $0.intensity.rawValue }
let vector_state = reports.map { $0.intensity }
let minutes: TimeInterval = filter.minutes
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 {
@@ -200,21 +255,7 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
}
let circles = Array(0..<cluster_code).map { (i) -> MapCircle in
var value_distance = max_distance[i] / 20.0
if value_distance > 1.0 {
value_distance = 1.0
}
let value_intensity = (cluster_intensity[i]-1.0) / 2.0
let value_reference = max(value_distance, value_intensity)
let color: UIColor
if value_reference <= 0.5 {
let red = round(value_reference * 510)
color = UIColor(red: CGFloat(red / 255.0), green: 230.0/255.0, blue: 0.0, alpha: 1.0)
} else {
let green = round(230 - (value_reference - 0.5) * 460)
color = UIColor(red: 255.0, green: CGFloat(green / 255.0), blue: 0.0, alpha: 1.0)
}
let color: UIColor = AppTheme.Colors.darkGray
let centre = CLLocation(latitude: lat_centre[i], longitude: lon_centre[i])
let farest = CLLocation(latitude: lat_farest[i], longitude: lon_farest[i])
@@ -240,29 +281,6 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
mapView.addOverlays(overlays)
}
private func getDeltaMinute(_ date: Date) -> TimeInterval {
Date().timeIntervalSince(date) / 60.0
}
private func openShareActivity(for report: EQNSegnalazione) {
// create message to share
let intensity = report.intensity.description
let difference = Int(Date().timeIntervalSince(report.date) / 60.0)
let time = EQNUtility.formattedString(forTimeDifference: difference)
let message = [
NSLocalizedString("share_hashtag", comment: ""),
intensity,
NSLocalizedString("share_felt", comment: ""),
report.address,
time + ".",
NSLocalizedString("share_notified", comment: "")
].joined(separator: " ")
let controller = UIActivityViewController(activityItems: [message], applicationActivities: [])
present(controller, animated: true)
}
// MARK: - Map
override func setupAnnotationView(for annotation: MKAnnotation, on mapView: MKMapView) -> MKAnnotationView? {
@@ -270,10 +288,17 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
return nil
}
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: EQNCustomAnnotationView.SingleLineIdentifier, for: annotation) as! EQNCustomAnnotationView
let identifier = appPreferences.userReportExpandedView ? EQNCustomAnnotationView.SingleLineIdentifier : EQNCustomAnnotationView.SmallIdentifier
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier, for: annotation) as! EQNCustomAnnotationView
annotationView.image = annotation.image
annotationView.title = annotation.title
let size = appPreferences.userReportExpandedView ? EQNCustomAnnotationView.SingleLineImageHeight : EQNCustomAnnotationView.SmallViewImageHeight
annotationView.image = annotation.image(with: size)
annotationView.title = annotation.timeDifference
annotationView.canShowCallout = true
// Psizioniamo più in alto le segnalazioni con intensità maggiore.
// Valori maggiori di anchorPointZ mettono la view più in basso,
// quindi invertiamo il valore dell'intensità
annotationView.layer.anchorPointZ = (1000 - CGFloat(annotation.report?.intensity ?? 0))
return annotationView
}
@@ -7,45 +7,111 @@
//
import UIKit
import Shogun
class SegnalazioniSendReportCell: EQNBaseTableViewCell {
class SegnalazioniSendReportCell: EQNBaseContainerTableViewCell {
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var descriptionLabel: UILabel!
@IBOutlet private weak var mildLabel: UILabel!
@IBOutlet private weak var mildButton: UIButton!
@IBOutlet private weak var strongLabel: UILabel!
@IBOutlet private weak var strongButton: UIButton!
@IBOutlet private weak var veryStrongLabel: UILabel!
@IBOutlet private weak var veryStrongButton: UIButton!
// MARK: - View Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
private struct Report: Equatable {
let magnitude: Int
let text: String
let color: UIColor
localizeUI()
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
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")!)
]
// MARK: - Internal
override func setupUI() {
super.setupUI()
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: "")
descriptionLabel.text = NSLocalizedString("manual_usebutton", comment: "")
mildLabel.text = NSLocalizedString("manual_mild", comment: "")
strongLabel.text = NSLocalizedString("manual_strong", comment: "")
veryStrongLabel.text = NSLocalizedString("manual_verystrong", 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
}
override func layoutSubviews() {
super.layoutSubviews()
let labels: [UILabel] = [mildLabel, strongLabel, veryStrongLabel]
labels.forEach { (label) in
label.layer.borderWidth = AppTheme.shared.buttonBorderWidth
label.layer.borderColor = AppTheme.shared.buttonBorderColor.cgColor
label.layer.cornerRadius = AppTheme.shared.buttonCornerRadius
label.clipsToBounds = true
}
// MARK: - Actions
@IBAction private func onTapReportButton(_ sender: UIButton) {
let magnitude = sender.tag
onTapReport(magnitude)
}
@objc private func onTapMagnitudeButton(_ sender: UIButton) {
let magnitude = sender.tag
onTapReport(magnitude)
}
}
@@ -38,19 +38,23 @@
- (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
{
[super refreshUI];
if ([self.userDefoult objectForKey:DATA_MESSAGE_EQN]){
NSDate *dateMessage = [self.userDefoult objectForKey:DATA_MESSAGE_EQN];
if ([EQNUtility getDifferenceMinute:dateMessage] >= EQNSendReportDelayBetweenComments){
[self.userDefoult removeObjectForKey:DATA_MESSAGE_EQN];
[self.userDefoult removeObjectForKey:CODE_MESSAGE_EQN];
if ([self.userDefoult objectForKey:NSUserDefaults.UserReportMessage]){
NSDate *dateMessage = [self.userDefoult objectForKey:NSUserDefaults.UserReportMessage];
if (![dateMessage isBeforeInterval:EQNSendReportDelayBetweenComments]) {
[self.userDefoult removeObjectForKey:NSUserDefaults.UserReportMessage];
[self.userDefoult removeObjectForKey:NSUserDefaults.UserReportCodeStatus];
}
}
@@ -70,43 +74,48 @@
return 2;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
switch (indexPath.row) {
case 0: return 140;
default: return UITableViewAutomaticDimension;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
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;
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ReportEarthquakeCell" forIndexPath:indexPath];
SegnalazioniSendReportCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ReportEarthquakeCell" forIndexPath:indexPath];
cell.onTapReport = ^(NSInteger magnitude) {
[self sendReportTappedWithMagnitude:magnitude];
};
return cell;
}
#pragma mark - Actions
- (IBAction)openMapTapped:(id)sender
- (void)openMap
{
SegnalazioniMapViewController *controller = [[SegnalazioniMapViewController alloc] init];
[self presentViewController:controller animated:YES completion:nil];
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];
@@ -117,12 +126,12 @@
[[EQNManager defaultManager] sincronizza];
}
- (IBAction)sendReportTapped:(UIButton *)sender
- (void)sendReportTappedWithMagnitude:(NSInteger)magnitude
{
// check to avoid multiple consecutive reports
if ([self.userDefoult objectForKey:CODE_MESSAGE_EQN]) {
NSDate *dateMessage = [self.userDefoult objectForKey:DATA_MESSAGE_EQN];
if ([EQNUtility getDifferenceMinute:dateMessage] <= EQNSendReportDelayBetweenMessages){
if ([self.userDefoult objectForKey:NSUserDefaults.UserReportCodeStatus]) {
NSDate *dateMessage = [self.userDefoult objectForKey:NSUserDefaults.UserReportMessage];
if (![dateMessage isBeforeInterval:EQNSendReportDelayBetweenMessages]) {
NSString *message = NSLocalizedString(@"manual_wait", @"");
[self showErrorAlertWithMessage:message];
[self performSelectorOnMainThread:@selector(sincronizzazione) withObject:nil waitUntilDone:YES];
@@ -133,9 +142,9 @@
// ask for user confirmation
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"attention", @"") message:NSLocalizedString(@"manual_sure", nil) preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"manual_yes", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[self executeSendReportWithMagnitude:sender.tag];
[self executeSendReportWithMagnitude:magnitude];
}]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"options_cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"status_cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
@@ -158,16 +167,14 @@
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:url richiesta:EQNTipoChiamataSegnalazioneTerremoto success:^(id result) {
[self.userDefoult setObject:result forKey:CODE_MESSAGE_EQN];
[self.userDefoult setObject:[NSDate date] forKey:DATA_MESSAGE_EQN];
[self.userDefoult setObject:result forKey:NSUserDefaults.UserReportCodeStatus];
[self.userDefoult setObject:[NSDate date] forKey:NSUserDefaults.UserReportMessage];
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"report", @"")
message:NSLocalizedString(@"manual_ok", @"")
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ok", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[self sendComment];
}]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ok", nil) style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
});
} failure:^(NSError * error) {
@@ -177,42 +184,6 @@
}];
}
- (void)sendComment
{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"manual_sendmessage_button" , @"")
message:NSLocalizedString(@"manual_sendmessage", @"")
preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
}];
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ok" ,@"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
UITextField * messaggio = alertController.textFields.firstObject;
NSURL *url = [EQNGeneratoreURLServer urlInvioCommentoTerremoto:messaggio.text codeMessage:[self.userDefoult objectForKey:CODE_MESSAGE_EQN]];
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:url richiesta:EQNTipoChiamataCommentoTerremoto success:^(id result) {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"manual_sendmessage_button" , @"")
message:NSLocalizedString(@"manual_message_received", @"")
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ok", nil) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
});
} failure:^(NSError * error) {
[self showErrorAlertWithMessage:error.localizedDescription];
}];
dispatch_async(dispatch_get_main_queue(), ^{
[self refreshUI];
});
}]];
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"options_cancel", @"") style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alertController animated:YES completion:nil];
}
#pragma mark - Private
- (void)showErrorAlertWithMessage:(NSString *)errorMessage
@@ -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)
}
}
@@ -9,34 +9,10 @@
import UIKit
import MapKit
import CoreLocation
import Shogun
protocol SeismicNetworkTableViewCellDelegate: AnyObject {
func seismicNetworkCellDidTapShare(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapWeather(_ cell: SeismicNetworkTableViewCell, hasValidWeatherData: Bool)
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 {
@@ -44,61 +20,38 @@ class SeismicNetworkTableViewCell: UITableViewCell {
case normal
/// Cell with map visible
case mapExpanded
/// Cell with weather info visible
case weatherExpanded
}
/// 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(for: .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
label.font = UIFont.preferredFont(for: .title2, weight: .semibold)
label.font = UIFont.preferredFont(forTextStyle: .title2, weight: .semibold)
label.numberOfLines = 3
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
}()
@@ -113,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
}()
@@ -149,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
}()
@@ -158,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
}()
@@ -173,20 +133,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
return mapView
}()
private lazy var weatherImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var weatherInfoLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = Self.DefaultBodyFontLight
return label
}()
// MARK: - Init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@@ -199,19 +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
translatesAutoresizingMaskIntoConstraints = false
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
@@ -233,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) {
@@ -304,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)
@@ -325,36 +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 buttonWeather = createRoundedButton(title: "🌤", action: #selector(weatherTapped(_:)))
stackViewButtons.addArrangedSubview(buttonWeather)
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
@@ -364,41 +303,19 @@ 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
previousView = mapView
} else if displayType == .weatherExpanded {
let weatherTitleLabel = UILabel()
weatherTitleLabel.translatesAutoresizingMaskIntoConstraints = false
weatherTitleLabel.text = NSLocalizedString("weather_weather", comment: "")
weatherTitleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
containerView.addSubview(weatherTitleLabel)
weatherTitleLabel.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
weatherTitleLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
weatherTitleLabel.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
containerView.addSubview(weatherInfoLabel)
containerView.addSubview(weatherImageView)
weatherImageView.heightAnchor.constraint(equalToConstant: 60.0).isActive = true
weatherImageView.widthAnchor.constraint(equalTo: weatherImageView.heightAnchor).isActive = true
weatherImageView.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
weatherImageView.trailingAnchor.constraint(equalTo: weatherInfoLabel.leadingAnchor, constant: -8.0).isActive = true
weatherImageView.centerYAnchor.constraint(equalTo: weatherInfoLabel.centerYAnchor).isActive = true
weatherInfoLabel.topAnchor.constraint(equalTo: weatherTitleLabel.bottomAnchor, constant: 4.0).isActive = true
weatherInfoLabel.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
previousView = weatherInfoLabel
}
if (displayType == .mapExpanded || displayType == .weatherExpanded) {
let buttonClose = createRoundedButton(title: NSLocalizedString("official_close", comment: "").uppercased(), action: #selector(closeTapped(_:)))
if (displayType == .mapExpanded) {
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
@@ -406,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() {
@@ -419,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)"
@@ -446,7 +358,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
alertsLabel.text = "⚠️ \(viewModel.users)"
}
if displayType == .mapExpanded {
// zoom based on population involved
let longitudeSpan = mapSpanLongitude(population: seismic.population100km)
@@ -463,14 +374,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(mapDetailTapped(_:)))
mapView.addGestureRecognizer(tapRecognizer)
} else if displayType == .weatherExpanded {
weatherInfoLabel.text = ""
+ String(format: NSLocalizedString("weather_temperature", comment: ""), seismic.weatherTemperature.doubleValue - EQNMathKelvin) + "\n"
+ String(format: NSLocalizedString("weather_pressure", comment: ""), seismic.weatherPressure) + "\n"
+ String(format: NSLocalizedString("weather_windspeed", comment: ""), seismic.weatherWindSpeed) + "\n"
+ String(format: NSLocalizedString("weather_humidity", comment: ""), seismic.weatherHumidity) + "\n"
+ String(format: NSLocalizedString("weather_clouds", comment: ""), seismic.weatherCloud)
weatherImageView.image = UIImage(named: "weather_\(seismic.weatherIcon).png")
}
}
@@ -481,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]
@@ -500,114 +408,48 @@ 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()
}
/// Creates a snapshot of the current cell
/// - Returns: Image with the snapshot of the cell
public func createSnapshot() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: contentView.bounds.size)
let image = renderer.image { ctx in
contentView.drawHierarchy(in: contentView.bounds, afterScreenUpdates: true)
}
return image
}
// 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 weatherTapped(_ sender: UIButton) {
if displayType != .weatherExpanded {
let validData = seismic?.weatherCode != nil
delegate?.seismicNetworkCellDidTapWeather(self, hasValidWeatherData: validData)
}
}
@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
@@ -620,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"
}
}
@@ -7,6 +7,7 @@
//
import UIKit
import Shogun
protocol SeismicFiltersViewControllerDelegate: AnyObject {
func seismicFiltersControllerDidUpdateFilters(_ controller: SeismicFiltersViewController)
@@ -15,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?
@@ -36,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("Distanza massima", 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
@@ -66,7 +58,8 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
super.viewDidLoad()
setupUI()
updateUI()
loadDataSource()
tableView.reloadData()
}
private func setupUI() {
@@ -83,21 +76,10 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
closeButton.setLocalizedTitle(key: "official_close", uppercased: false)
}
private func updateUI() {
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
tableView.reloadData()
private func loadDataSource() {
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
@@ -107,81 +89,75 @@ 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 = tableView.dequeueReusableCell(withIdentifier: SettingSliderTableViewCell.Identifier, for: indexPath) as! SettingSliderTableViewCell
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()
updateUI()
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()
updateUI()
}
} 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()
updateUI()
}
} 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()
updateUI()
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
case .enable:
let cell = tableView.dequeueReusableCell(withIdentifier: SettingEnableTableViewCell.Identifier, for: indexPath) as! SettingEnableTableViewCell
let cell = SettingEnableTableViewCell(style: .default, reuseIdentifier: nil)
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()
updateUI()
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()
updateUI()
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()
updateUI()
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
@@ -194,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
@@ -52,11 +103,11 @@ struct SeismicNetworkViewModel {
// distance
let distanceRounded = Int(round(seismic.userDistance))
self.distance = String(format: NSLocalizedString("timer_message2_other", comment: ""), distanceRounded)
self.distance = String(format: NSLocalizedString("official_distance", comment: ""), distanceRounded)
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
}
@@ -8,17 +8,25 @@
import UIKit
import MapKit
import Shogun
protocol SeismicNetworksMapDetailViewControllerDelegate: AnyObject {
func seismicNetworksMapDetailControllerWillUpdateData(_ controller: SeismicNetworksMapDetailViewController, needsDataUpdate: Bool)
}
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?
@@ -41,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
}()
@@ -75,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]) {
@@ -83,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() {
@@ -91,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()
}
@@ -107,6 +132,8 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
}
override func didTapAnnotation(_ annotation: MKAnnotation) {
mapView.deselectAnnotation(annotation, animated: true)
guard let annotation = annotation as? EQNMapAnnotationSeismic else { return }
let viewModel = SeismicNetworkViewModel(seismic: annotation.seismic)
@@ -124,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
@@ -159,16 +216,13 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
mapView.addOverlays(circles)
}
// MARK: - Actions
@objc override func filtersTapped(_ sender: UIGestureRecognizer) {
let controller = SeismicFiltersViewController.makeController()
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? {
@@ -176,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 {
@@ -193,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)
@@ -9,28 +9,29 @@
import UIKit
import EventKitUI
import DZNEmptyDataSet
import Shogun
class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
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
@@ -39,27 +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?
/// Index path of row with weather expanded
private var openWeatherIndexPath: 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)
}
@@ -68,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?) {
@@ -86,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
@@ -107,16 +248,22 @@ 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) {
self.openMapIndexPath = nil
self.openWeatherIndexPath = nil
DispatchQueue.main.async {
self.refreshUI()
@@ -128,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) {
@@ -154,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() {
@@ -166,6 +319,9 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
if let mapController = currentMapController {
mapController.updateSeismics(filteredSeismics)
}
scrollIndicatorView.seismics = seismicViewModels
currentCenteredIndexPath = nil
}
private func getSeismics() -> [EQNSisma] {
@@ -178,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) {
@@ -187,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()
}
@@ -213,20 +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
} else if openWeatherIndexPath == indexPath {
type = .weatherExpanded
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
}
@@ -241,6 +706,12 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
}
}
// MARK: - UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateCenterCellIndexPath()
}
// MARK: - Private
private func openCalendar(for seismic: EQNSisma) {
@@ -296,28 +767,28 @@ 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.createSnapshot()
let snapshot = cell.contentView.createSnapshot()
// text to share with the snapshot
let shareHashtag = NSLocalizedString("share_hashtag", comment: "")
@@ -330,59 +801,44 @@ extension SeismicNetworksViewController: SeismicNetworkTableViewCellDelegate {
present(controller, animated: true)
}
func seismicNetworkCellDidTapWeather(_ cell: SeismicNetworkTableViewCell, hasValidWeatherData: Bool) {
guard let index = tableView?.indexPath(for: cell) else { return }
if !hasValidWeatherData {
let alert = UIAlertController(title: NSLocalizedString("attention", comment: ""),
message: NSLocalizedString("weather_nodata", comment: ""),
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("ok", comment: ""), style: .default))
present(alert, animated: true)
return
}
let indexToReloads = [openMapIndexPath, openWeatherIndexPath, index].compactMap { $0 }
openWeatherIndexPath = index
openMapIndexPath = nil
tableView?.reloadRows(at: indexToReloads, with: .automatic)
}
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, openWeatherIndexPath, index].compactMap { $0 }
let indexToReloads = [openMapIndexPath, index].compactMap { $0 }
openMapIndexPath = index
openWeatherIndexPath = nil
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, openWeatherIndexPath, index].compactMap { $0 }
let indexToReloads = [openMapIndexPath, index].compactMap { $0 }
openMapIndexPath = nil
openWeatherIndexPath = nil
tableView?.reloadRows(at: indexToReloads, with: .automatic)
tableView.reloadRows(at: indexToReloads, with: .automatic)
}
}
@@ -400,22 +856,6 @@ extension SeismicNetworksViewController: SeismicFiltersViewControllerDelegate {
}
}
extension SeismicNetworksViewController: SeismicSettingsViewControllerDelegate {
func seismicSettingsControllerDidComplete(_ controller: SeismicSettingsViewController) {
refreshUI()
}
func seismicSettingsControllerWillOpenProviders(_ controller: SeismicSettingsViewController) {
performSegue(withIdentifier: Self.SegueIdentifierSeismicNetworks, sender: nil)
}
}
extension SeismicNetworksViewController: SeismicSettingsNetworksViewControllerDelegate {
func seismicSettingsNetworksControllerDidComplete(_ controller: SeismicSettingsNetworksViewController) {
refreshUI()
}
}
extension SeismicNetworksViewController: SeismicCardSettingsViewControllerDelegate {
func seismicCardSettingsDidComplete(_ controller: SeismicCardSettingsViewController) {
refreshUI()
@@ -423,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,112 +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
if let savedNetworks = UserDefaults.standard.object(forKey: IMPOSTAZIONE_ENTI_RETI_SISMICHEI) as? [String] {
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)
}
tableView.reloadRows(at: [indexPath], with: .automatic)
}
// MARK: - Actions
@IBAction func cancelTapped(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
@IBAction func saveTapped(_ sender: Any) {
// save selected networks
UserDefaults.standard.set(savedNetworks, forKey: IMPOSTAZIONE_ENTI_RETI_SISMICHEI)
// 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: "options_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]
UserDefaults.standard.set(selectedNetworks, forKey: IMPOSTAZIONE_ENTI_RETI_SISMICHEI)
// 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("options_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)
}
@@ -89,8 +89,6 @@ class SettingDateTableViewCell: UITableViewCell {
// MARK: - Private
private func setupUI() {
translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(valuesLabel)
@@ -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
@@ -62,7 +71,6 @@ class SettingEnableTableViewCell: UITableViewCell {
private func setupUI() {
selectionStyle = .none
translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
@@ -76,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
@@ -84,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
@@ -54,8 +54,6 @@ class SettingMultivaluesTableViewCell: UITableViewCell {
// MARK: - Private
private func setupUI() {
translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
@@ -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)
@@ -55,7 +55,6 @@ class SettingSegmentedTableViewCell: UITableViewCell {
private func setupUI() {
selectionStyle = .none
translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
@@ -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)
@@ -66,7 +66,6 @@ class SettingSliderTableViewCell: UITableViewCell {
private func setupUI() {
selectionStyle = .none
translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
@@ -99,6 +98,12 @@ class SettingSliderTableViewCell: UITableViewCell {
let index = Int(sender.value)
let item = items[index]
// make slider with step values
let step: Float = 1
let roundedValue = round(sender.value / step) * step
sender.value = roundedValue
// update value
valueLabel.text = item.display
valueChanged?(item)
}
@@ -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,304 +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 (strong, nonatomic) NSArray<EQNGenericValue *> *dataSourceSismi;
@property (nonatomic, strong) NSArray<EQNGenericValue *> *dataSourceRaggioSisma;
@property (nonatomic, strong) EQNGenericValue *currentSeismicToNotify;
@property (strong, nonatomic) EQNGenericValue *currentLowSeismicRadius;
@property (strong, nonatomic) EQNGenericValue *currentStrongSeismicRadius;
@property (strong, nonatomic) NSDate *currentStartTime;
@property (nonatomic) BOOL isStartTimeExpanded;
@property (strong, nonatomic) NSDate *currentEndTime;
@property (nonatomic) BOOL isEndTimeExpanded;
@property (nonatomic, strong) NSDateFormatter *dateFormatter;
@property (nonatomic, assign) BOOL notificationEnabled;
@property (nonatomic, assign) BOOL criticalAlertsEnabled;
@property (nonatomic, assign) BOOL doNotDisturbEnabled;
@end
@implementation SettingsRealTimeAlertsViewController
typedef NS_ENUM(NSInteger, RowIdentifier) {
RowIdentifierAbilitaNotifiche = 0,
RowIdentifierAbilitaCriticalAlerts,
RowIdentifierSismiDaNotificare,
RowIdentifierRaggioSismiLievi,
RowIndntifierRaggioSismiForti,
RowIdentifierNonDisturbare,
RowIdentifierNonDisturbareOraInizio,
RowIdentifierNonDisturbareOraFine
};
#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", @"")],
[[SettingItem alloc] initWithType:SettingTypeSegmented title:NSLocalizedString(@"options_notification_eqn_intensity", @"") subtitle:NSLocalizedString(@"", @"")],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_radius_mild", @"")],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_radius_strong", @"")]
];
self.dataSourceSismi = [EQNData seismicToNotify];
self.dataSourceRaggioSisma = [EQNData raggioSismi];
[self updateUI];
}
#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)updateUI
{
self.notificationEnabled = [EQNAllertaSismica sharedInstance].isAbilitato;
self.criticalAlertsEnabled = [EQNAllertaSismica sharedInstance].isCriticalAlertsEnabled;
self.doNotDisturbEnabled = [EQNAllertaSismica sharedInstance].isintervalloAllarme;
// sismi da notificare
EQNGenericValue *sismiDaNotificare = [EQNData seismicToNotifyFor:[EQNAllertaSismica sharedInstance].sismiDaNotificare];
self.currentSeismicToNotify = sismiDaNotificare;
// raggio sismi lievi
EQNGenericValue *raggioSismiLievi = [EQNData raggioSismaFor:[EQNAllertaSismica sharedInstance].raggioSismiLievi];
self.currentLowSeismicRadius = raggioSismiLievi;
// raggio sismi forti
EQNGenericValue *raggioSismiForti = [EQNData raggioSismaFor:[EQNAllertaSismica sharedInstance].raggioSismiForti];
self.currentStrongSeismicRadius = raggioSismiForti;
// non disturbare, orari
NSDate *startTime = [EQNData doNotDisturbEndDateFrom:[EQNAllertaSismica sharedInstance].oraioInizio];
self.currentStartTime = startTime;
NSDate *endTime = [EQNData doNotDisturbEndDateFrom:[EQNAllertaSismica sharedInstance].orarioFine];
self.currentEndTime = endTime;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self.tableView reloadData];
}
#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];
};
} else if (indexPath.row == RowIdentifierNonDisturbare) {
cell.toggleSwitch.on = self.doNotDisturbEnabled;
cell.isDisabled = !self.notificationEnabled;
cell.valueChanged = ^(BOOL enabled) {
self.doNotDisturbEnabled = enabled;
[EQNAllertaSismica sharedInstance].isintervalloAllarme = self.doNotDisturbEnabled;
[[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;
if (indexPath.row == RowIdentifierSismiDaNotificare) {
cell.isDisabled = !self.notificationEnabled;
[cell configureControlWith:self.dataSourceSismi current:self.currentSeismicToNotify];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateSismicToNotify:item];
};
}
return cell;
} else if (setting.type == SettingTypeSlider) {
SettingSliderTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingSliderTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
if (indexPath.row == RowIdentifierRaggioSismiLievi) {
cell.isDisabled = !self.notificationEnabled;
[cell configureSliderWith:self.dataSourceRaggioSisma current:self.currentLowSeismicRadius];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateLowSeismicRadius:item];
};
} else if (indexPath.row == RowIndntifierRaggioSismiForti) {
cell.isDisabled = !self.notificationEnabled;
[cell configureSliderWith:self.dataSourceRaggioSisma current:self.currentStrongSeismicRadius];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateStrongSeismicRadius:item];
};
}
return cell;
} else if (setting.type == SettingTypeDate) {
SettingDateTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingDateTableViewCell.Identifier forIndexPath:indexPath];
cell.isDisabled = !self.doNotDisturbEnabled || !self.notificationEnabled;
cell.userInteractionEnabled = self.doNotDisturbEnabled && self.notificationEnabled;
cell.titleLabel.text = setting.title;
if (indexPath.row == RowIdentifierNonDisturbareOraInizio) {
cell.isPickerVisible = self.isStartTimeExpanded;
[cell updateDate:self.currentStartTime];
cell.valuesLabel.text = [self.dateFormatter stringFromDate:self.currentStartTime];
cell.valueChanged = ^(NSDate *date) {
[self updateStartTime:date];
};
} else if (indexPath.row == RowIdentifierNonDisturbareOraFine) {
cell.isPickerVisible = self.isEndTimeExpanded;
[cell updateDate:self.currentEndTime];
cell.valuesLabel.text = [self.dateFormatter stringFromDate:self.currentEndTime];
cell.valueChanged = ^(NSDate *date) {
[self updateEndTime:date];
};
}
return cell;
}
return nil;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == RowIdentifierNonDisturbareOraInizio) {
self.isStartTimeExpanded = !self.isStartTimeExpanded;
} else if (indexPath.row == RowIdentifierNonDisturbareOraFine) {
self.isEndTimeExpanded = !self.isEndTimeExpanded;
}
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
#pragma mark - Private
- (void)updateSismicToNotify:(EQNGenericValue *)seismic
{
[EQNAllertaSismica sharedInstance].sismiDaNotificare = seismic.value;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self updateUI];
}
- (void)updateLowSeismicRadius:(EQNGenericValue *)radius
{
[EQNAllertaSismica sharedInstance].raggioSismiLievi = radius.value;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self updateUI];
}
- (void)updateStrongSeismicRadius:(EQNGenericValue *)radius
{
[EQNAllertaSismica sharedInstance].raggioSismiForti = radius.value;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self updateUI];
}
- (void)updateStartTime:(NSDate *)date
{
[EQNAllertaSismica sharedInstance].oraioInizio = date;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self updateUI];
}
- (void)updateEndTime:(NSDate *)date
{
[EQNAllertaSismica sharedInstance].orarioFine = date;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self updateUI];
}
- (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 updateUI];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self updateUI];
}
#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)updateUI
{
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];
[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_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 updateUI];
}
- (void)updateSeismicEnergy:(EQNGenericValue *)energy
{
[EQNNotificheReteSismiche sharedInstance].energiaSisma = energy.value;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self updateUI];
}
- (void)updateStrongEarthquakeEnergy:(EQNGenericValue *)energy
{
[EQNNotificheReteSismiche sharedInstance].energiaTerremotiForti = energy.value;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self updateUI];
}
- (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()
}
}
@@ -27,10 +27,15 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
!availableFilters.isEmpty
}
/// If `true` the close button will be shown on top of the map view
var isCloseButtonVisible: Bool {
true
}
// MARK: - Internal
/// Annotations displayed on the map
private var mapAnnotations = [MKAnnotation]()
private(set) var mapAnnotations = [MKAnnotation]()
/// If `true`, the initial filter has been already evaluated
private var initialFilterEvaluated = false
@@ -99,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() {
@@ -116,13 +149,14 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
private func setupUI() {
view.backgroundColor = .white
view.addSubview(mapView)
view.addSubview(closeButton)
view.addSubview(containerView)
if isFilterViewVisible {
view.addSubview(filtersView)
}
closeButton.addDefaultConstraint(to: view)
if isCloseButtonVisible {
view.addSubview(closeButton)
closeButton.addDefaultConstraint(to: view)
}
containerView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
@@ -145,6 +179,20 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
mapView.bottomAnchor.constraint(equalTo: (isFilterViewVisible ? filtersView : containerView).topAnchor).isActive = true
extraUI()
}
private func addWatermarkView() {
view.addSubview(watermarkView)
watermarkView.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 10.0).isActive = true
watermarkView.leadingAnchor.constraint(equalTo: mapView.leadingAnchor, constant: 10.0).isActive = true
watermarkView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
}
private func removeWatermarkView() {
watermarkView.removeFromSuperview()
}
// MARK: - View Lifecycle
@@ -152,6 +200,7 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
registerMapAnnotationViews()
loadDataSource()
}
@@ -167,6 +216,16 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
// MARK: - Public
/// Add extra UI not available in the base class
func extraUI() {
// nope, subclass will implement some logic
}
/// Configure UI after view initialization
func configureUI() {
// nope, subclass will implement some logic
}
/// Load data to display on the map
func loadDataSource() {
// nope, subclass will implement some logic
@@ -219,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)
@@ -230,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() {
@@ -249,7 +339,7 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
self.applyFilter(filter)
}))
}
sheet.addAction(UIAlertAction(title: NSLocalizedString("options_cancel", comment: ""), style: .cancel, handler: nil))
sheet.addAction(UIAlertAction(title: NSLocalizedString("status_cancel", comment: ""), style: .cancel, handler: nil))
present(sheet, animated: true, completion: nil)
}
@@ -267,6 +357,7 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
}
let annotationView = setupAnnotationView(for: annotation, on: mapView)
annotationView?.zPriority = zPriority(for: annotation)
return annotationView
}
@@ -277,7 +368,5 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let annotation = view.annotation else { return }
didTapAnnotation(annotation)
mapView.deselectAnnotation(annotation, animated: true)
}
}
@@ -8,6 +8,7 @@
import UIKit
import MapKit
import Shogun
class AlertSimulatorViewController: UIViewController, MKMapViewDelegate {
@@ -243,7 +244,7 @@ class AlertSimulatorViewController: UIViewController, MKMapViewDelegate {
}
private func navigateToSubscriptions() {
let controller = SubscriptionsViewController.makeController()
let controller = SubscriptionsViewController()
let navigationController = UINavigationController(rootViewController: controller)
present(navigationController, animated: true)
}
@@ -1,21 +0,0 @@
//
// Costanti+Extensions.swift
// Earthquake Network
//
// Created by Andrea Busi on 06/03/21.
// Copyright © 2021 Earthquake Network. All rights reserved.
//
import Foundation
extension EQNPastquakeIntensity {
var description: String {
switch self {
case .mild: return NSLocalizedString("widget_mild", comment: "")
case .strong: return NSLocalizedString("widget_strong", comment: "")
case .veryStrong: return NSLocalizedString("widget_verystring", comment: "")
}
}
}

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