Compare commits

...

231 Commits

Author SHA1 Message Date
Andrea Busi f85c60fdda release: Increase version for release 2025-07-24 22:07:38 +02:00
Andrea Busi 5f2a083789 fix: Update Twitter to X 2025-07-24 22:07:19 +02:00
Andrea Busi 9e54f74847 release: Increase version for release 2025-07-24 20:44:44 +02:00
Andrea Busi b6f9232f56 fix: Set color based on shake intensity 2025-07-24 20:44:11 +02:00
Andrea Busi dee14dea0f release: Increase version for release 2025-07-24 20:43:57 +02:00
Andrea Busi db0bde2f59 refactor: Remove no longer needed checks for iOS < 15 2025-07-24 16:46:29 +02:00
Andrea Busi 79d0d27ec5 feat: Rewrite NotificationContent in Swift and add wave animation
Resolves: https://github.com/futurainnovation/eqn.ios/issues/4
2025-07-24 16:44:55 +02:00
Andrea Busi 68012ec406 refactor: Improve notification content 2025-07-24 11:56:49 +02:00
Andrea Busi 59feb7699b release: Increase version for release 2025-07-18 17:08:19 +02:00
Andrea Busi 388f4e8b89 feat: Add new felt value as provider parameter
Resolves: https://github.com/futurainnovation/eqn.ios/issues/1
2025-07-18 17:04:29 +02:00
Andrea Busi ca3c9ebd83 refactor: Always use updateFilter method when updating filter values 2025-07-18 17:03:34 +02:00
Andrea Busi f23c19bce7 feat: Improve UI in notification content
Resolves: https://github.com/futurainnovation/eqn.ios/issues/4
2025-07-18 17:02:52 +02:00
Andrea Busi 276fa2032a release: Increase version for release 2025-07-18 12:44:30 +02:00
Andrea Busi 09f0d4d4d8 dependency: Bump Firebase 2025-07-18 12:44:22 +02:00
Andrea Busi 25f061ad5a release: Update changelog 2025-07-18 12:40:59 +02:00
Andrea Busi b9d9f7579c feat: Add filters recap view in seismic networks
Resolves: https://github.com/futurainnovation/eqn.ios/issues/2
2025-07-18 12:40:59 +02:00
Andrea Busi 39f5ff0249 fix: Use extensions from Shogun 2025-07-18 12:28:22 +02:00
Andrea Busi af68d70be5 feat: Increase minimum target to iOS 15 2025-07-18 12:28:22 +02:00
Andrea Busi dab999a78d dependency: Bump Shogun to v2.0 2025-07-18 12:28:22 +02:00
Andrea Busi f5ede5c26d refactor: Use Roman numerals in shakemap labels
Resolves: https://github.com/futurainnovation/eqn.ios/issues/3
2025-07-17 15:52:09 +02:00
Andrea Busi 6d4c1eb979 fix: Check array bounds to avoid crash 2025-05-15 10:31:41 +02:00
Andrea Busi 9bf6b75dac release: Increase version for release 2025-05-02 10:31:57 +02:00
Andrea Busi 69b83e9944 feat: Add settings to disable alert sound for mild quakes
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/89
2025-05-02 10:31:57 +02:00
Andrea Busi 5061e36a45 refactor: Reorganize some code 2025-05-02 10:31:57 +02:00
Andrea Busi 8919f3c08f refactor: Remove some unused @objc 2025-05-02 10:31:57 +02:00
Andrea Busi 9cf9ef8a64 feat: Use cache endpoint and round values to use server cache
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/90
2025-05-02 10:31:57 +02:00
Andrea Busi 9ee3a478f0 dependency: Update Firebase 2025-05-02 10:31:57 +02:00
Andrea Busi 8744595b56 release: Increase version for release 2025-04-25 22:32:54 +02:00
Andrea Busi fa05d6b5c4 fix: Rework init to avoid crash when server returns null values 2025-04-25 22:31:44 +02:00
Andrea Busi 471ccc9e4a release: Increase version for release 2025-04-24 12:49:25 +02:00
Andrea Busi 55de6f5ba0 refactor: Use project to set version and build number 2025-04-24 12:49:08 +02:00
Andrea Busi b12a9cc476 fix: Solve wrong sort in subscriptions 2025-04-24 12:46:53 +02:00
Andrea Busi a2f740b0a8 release: Increase version for release 2025-03-07 17:57:54 +01:00
Andrea Busi 9cf93e652b fix: Reset cell informations 2025-03-07 15:38:51 +01:00
Andrea Busi 2d23056ba8 release: Increase version for release 2025-03-07 14:10:47 +01:00
Andrea Busi cb6ecca774 refactor: Hide description label when taking screenshot in intensity map 2025-03-07 14:10:47 +01:00
Andrea Busi 96286a49f6 feat: Upgrade Xcode project version 2025-03-07 14:00:28 +01:00
Andrea Busi 481e8a28c0 fix: Solve crash with a single shakemape curve 2025-03-07 14:00:16 +01:00
Andrea Busi 286a4062f5 release: Increase version for release 2025-03-07 11:16:35 +01:00
Andrea Busi 01a8ad7419 fix: Solve wrong calculation in scroll indicator 2025-03-07 11:16:07 +01:00
Andrea Busi 6e97e9bd2c release: Increase version for release 2025-03-07 09:56:03 +01:00
Andrea Busi af6e94efcb fix: Solve non working intensity map in minimal cell 2025-03-07 09:45:45 +01:00
Andrea Busi 5387758449 fix: Select yearly as default subscription 2025-03-07 09:42:26 +01:00
Andrea Busi 054603b42d release: Increase version for release 2025-03-06 17:57:41 +01:00
Andrea Busi caf0e3b7cc feat: Add new minimal card in seismics list
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/87
2025-03-06 17:57:07 +01:00
Andrea Busi 4c35c38cc5 refactor: Move card informations to AppPreferences 2025-03-06 15:49:26 +01:00
Andrea Busi 521254c8c1 refactor: Remove unused constant 2025-03-06 13:01:20 +01:00
Andrea Busi 78a1710584 refactor: Replace deprecated methods 2025-03-06 10:59:36 +01:00
Andrea Busi b2a54a544c feat: Update layout in Seismic List section
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/86
2025-03-06 10:45:02 +01:00
Andrea Busi 0f5ad24744 feat: Update layout in Subscriptions section
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/85
2025-03-06 10:19:14 +01:00
Andrea Busi 0296cd50cd feat: Update layout in Reports section
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/84
2025-03-06 10:18:44 +01:00
Andrea Busi 7551988b4e feat: Update layout in Alerts section
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/83
2025-03-06 10:17:42 +01:00
Andrea Busi 5edcaaad99 feat: Change layout for base container card (center title) 2025-03-06 10:17:08 +01:00
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
164 changed files with 8361 additions and 6297 deletions
+38 -2
View File
@@ -1,5 +1,43 @@
# Changelog
## 5.11
- Numeri romani in etichette shakemaps
- Aggiunta barra di recap dei filtri in Lista Sismi
- Riscritta NotificationContent in Swift
- Aggiunta animazione onda sismica in NotificationContent (+ altre modifiche grafiche)
## 5.10
- Usato endpoint cache per distquake_download_areacheck
- Aggiunta impostazione per non riprodurre suono notifiche per simsi deboli
## Versione 5.9.1
- Corretto ordinamento in sottoscrizioni attive (prima Top10k)
- Modificato parsing per risolvere crash con valori nulli
## Versione 5.9
- Aggiunta barra laterale in lista sismi
- Aggiunto filtro "sismi percepiti"
- 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
@@ -13,8 +51,6 @@
- Rivista gestione notifiche push allerte
- Migrate costanti in nuova struttura Swift
## Versione 5.4
- Aggiunto SDK Facebook
+34
View File
@@ -0,0 +1,34 @@
{
"magnitude_range" : "0",
"google.c.a.e" : "1",
"provider" : "INGV",
"google.c.fid" : "fFjFx_Em8E-op_zHYXZpSr",
"preliminary" : "0",
"longitude" : "15.2917",
"gcm.message_id" : "1719991146578422",
"latitude" : "40.7738",
"google.c.sender.id" : "899482329945",
"type" : "official",
"magnitude" : "1.4",
"difference" : "13",
"depth" : "14.6",
"aps" : {
"mutable-content" : 1,
"alert" : {
"body" : "Sisma rilevato a",
"title-loc-key" : "Segnalazione da rete sismica",
"title" : "Segnalazione da rete sismica",
"loc-key" : "Sisma rilevato a",
"action-loc-key" : "",
"loc-args" : [
"2 km SW Laviano (SA) - M1.4"
]
},
"content-available" : 1,
"sound" : "default"
},
"data" : "2024-07-03 09:05:52",
"magnitude_type" : "ML",
"place" : "2 km SW Laviano (SA)",
"pop100" : "6824"
}
-31
View File
@@ -1,31 +0,0 @@
{
"magnitude_range" : "0",
"provider" : "SGC",
"google.c.a.e" : "1",
"google.c.fid" : "d3PS1dEvrUA-tmLLpl5E5f",
"preliminary" : "0",
"longitude" : "-75.5157",
"gcm.message_id" : "1668682445010677",
"latitude" : "4.35306",
"type" : "official",
"google.c.sender.id" : "899482329945",
"difference" : "6",
"data" : "2022-11-17 11:48:00",
"depth" : "26",
"aps" : {
"content-available" : 1,
"alert" : {
"loc-key" : "Sisma rilevato a",
"title-loc-key" : "Segnalazione da rete sismica",
"loc-args" : [
"Cajamarca - Tolima, Colombia - M2.2"
]
},
"mutable-content" : 1,
"sound" : "default"
},
"magnitude" : "2.2",
"magnitude_type" : "M",
"place" : "Cajamarca - Tolima, Colombia",
"pop100" : "6622"
}
@@ -5,7 +5,7 @@
"loc-args": [
"2 km da Foligno"
],
"loc-key": "Rilevato sisma forte a",
"loc-key": "Sisma segnalato da utente a",
"title-loc-key": "Allerta sismica in tempo reale"
},
"category": "notifica_con_mappa",
@@ -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
}
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="M4Y-Lb-cyx">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="M4Y-Lb-cyx">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -11,37 +11,45 @@
<!--Notification View Controller-->
<scene sceneID="cwh-vc-ff4">
<objects>
<viewController id="M4Y-Lb-cyx" userLabel="Notification View Controller" customClass="NotificationViewController" sceneMemberID="viewController">
<viewController id="M4Y-Lb-cyx" userLabel="Notification View Controller" customClass="NotificationContentViewController" customModule="EQNNotificationContent" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" simulatedAppContext="notificationCenter" id="S3S-Oj-5AN">
<rect key="frame" x="0.0" y="0.0" width="320" height="330"/>
<rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pCT-Wh-lut">
<rect key="frame" x="8" y="216" width="304" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pCT-Wh-lut">
<rect key="frame" x="8" y="350" width="304" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bT3-3m-qLh">
<rect key="frame" x="8" y="285" width="304" height="29"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bT3-3m-qLh">
<rect key="frame" x="8" y="417" width="304" height="53"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/>
<color key="textColor" red="0.91764705879999997" green="0.46274509800000002" blue="0.0078431372550000003" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<mapView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" mapType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="4ID-Zb-OQF">
<rect key="frame" x="0.0" y="0.0" width="320" height="200"/>
<mapView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" mapType="standard" showsUserLocation="YES" translatesAutoresizingMaskIntoConstraints="NO" id="4ID-Zb-OQF">
<rect key="frame" x="0.0" y="20" width="320" height="320"/>
<constraints>
<constraint firstAttribute="height" constant="200" id="Pgl-8e-ePq"/>
<constraint firstAttribute="width" secondItem="4ID-Zb-OQF" secondAttribute="height" multiplier="1:1" id="yXb-UG-FZY"/>
</constraints>
<connections>
<outlet property="delegate" destination="M4Y-Lb-cyx" id="Cs2-OY-eT2"/>
</connections>
</mapView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f3d-th-bgU">
<rect key="frame" x="8" y="244.5" width="304" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f3d-th-bgU">
<rect key="frame" x="8" y="380.5" width="304" height="26.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="s7c-ag-zBA" customClass="EQNBlurredCloseButton" customModule="EQNNotificationContent" customModuleProvider="target">
<rect key="frame" x="103" y="26" width="114" height="35"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="Tap to open"/>
<connections>
<action selector="openAppTapped:" destination="M4Y-Lb-cyx" eventType="touchUpInside" id="Sw3-xS-cXi"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="2BE-c3-nQJ"/>
<constraints>
@@ -49,23 +57,26 @@
<constraint firstItem="f3d-th-bgU" firstAttribute="leading" secondItem="pCT-Wh-lut" secondAttribute="leading" id="7qA-vV-ocI"/>
<constraint firstItem="2BE-c3-nQJ" firstAttribute="trailing" secondItem="pCT-Wh-lut" secondAttribute="trailing" constant="8" id="CAC-UM-SaJ"/>
<constraint firstItem="f3d-th-bgU" firstAttribute="trailing" secondItem="pCT-Wh-lut" secondAttribute="trailing" id="Dd7-BF-iOG"/>
<constraint firstItem="f3d-th-bgU" firstAttribute="top" secondItem="pCT-Wh-lut" secondAttribute="bottom" constant="8" id="FJ8-nn-ydU"/>
<constraint firstItem="2BE-c3-nQJ" firstAttribute="bottom" secondItem="bT3-3m-qLh" secondAttribute="bottom" constant="16" id="I71-6U-jK3"/>
<constraint firstItem="pCT-Wh-lut" firstAttribute="top" secondItem="4ID-Zb-OQF" secondAttribute="bottom" constant="16" id="It9-RA-906"/>
<constraint firstItem="f3d-th-bgU" firstAttribute="top" secondItem="pCT-Wh-lut" secondAttribute="bottom" constant="10" id="FJ8-nn-ydU"/>
<constraint firstItem="s7c-ag-zBA" firstAttribute="top" secondItem="2BE-c3-nQJ" secondAttribute="top" constant="6" id="Gsp-ye-V4M"/>
<constraint firstItem="2BE-c3-nQJ" firstAttribute="bottom" secondItem="bT3-3m-qLh" secondAttribute="bottom" constant="10" id="I71-6U-jK3"/>
<constraint firstItem="pCT-Wh-lut" firstAttribute="top" secondItem="4ID-Zb-OQF" secondAttribute="bottom" constant="10" id="It9-RA-906"/>
<constraint firstItem="bT3-3m-qLh" firstAttribute="trailing" secondItem="f3d-th-bgU" secondAttribute="trailing" id="KXf-x4-iZs"/>
<constraint firstItem="bT3-3m-qLh" firstAttribute="leading" secondItem="f3d-th-bgU" secondAttribute="leading" id="QlJ-Vh-oi4"/>
<constraint firstItem="bT3-3m-qLh" firstAttribute="top" secondItem="f3d-th-bgU" secondAttribute="bottom" constant="20" id="UUO-2F-eE7"/>
<constraint firstItem="bT3-3m-qLh" firstAttribute="top" secondItem="f3d-th-bgU" secondAttribute="bottom" constant="10" id="UUO-2F-eE7"/>
<constraint firstItem="4ID-Zb-OQF" firstAttribute="trailing" secondItem="2BE-c3-nQJ" secondAttribute="trailing" id="buf-BU-I5b"/>
<constraint firstItem="4ID-Zb-OQF" firstAttribute="leading" secondItem="2BE-c3-nQJ" secondAttribute="leading" id="e8D-ji-t64"/>
<constraint firstItem="4ID-Zb-OQF" firstAttribute="top" secondItem="2BE-c3-nQJ" secondAttribute="top" id="hL6-gc-S6i"/>
<constraint firstItem="s7c-ag-zBA" firstAttribute="centerX" secondItem="S3S-Oj-5AN" secondAttribute="centerX" id="mKj-2O-QDw"/>
</constraints>
</view>
<extendedEdge key="edgesForExtendedLayout"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<size key="freeformSize" width="320" height="330"/>
<size key="freeformSize" width="320" height="480"/>
<connections>
<outlet property="descriptionLabel" destination="f3d-th-bgU" id="Aym-KJ-DqY"/>
<outlet property="mapView" destination="4ID-Zb-OQF" id="x8o-nT-bL4"/>
<outlet property="tapToOpenButton" destination="s7c-ag-zBA" id="4aZ-hT-1vc"/>
<outlet property="titleLabel" destination="pCT-Wh-lut" id="uIg-dn-Wms"/>
<outlet property="waveLabel" destination="bT3-3m-qLh" id="AkJ-nd-d2R"/>
</connections>
@@ -0,0 +1,144 @@
//
// NotificationContentViewController.swift
// EQNNotificationContent
//
// Created by Andrea Busi on 24/07/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import UIKit
import MapKit
import UserNotifications
import UserNotificationsUI
class NotificationContentViewController: UIViewController {
// MARK: - UI
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var descriptionLabel: UILabel!
@IBOutlet private weak var waveLabel: UILabel!
@IBOutlet private weak var mapView: MKMapView!
@IBOutlet private weak var tapToOpenButton: UIButton!
private var animator: MapSeismicWaveAnimator?
// MARK: - View
override func viewDidLoad() {
super.viewDidLoad()
let tapRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(openAppTapped(_:))
)
view.addGestureRecognizer(tapRecognizer)
mapView
.register(
EQNCustomAnnotationView.self,
forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier
)
mapView
.register(
EQNCustomAnnotationView.self,
forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SmallIdentifier
)
tapToOpenButton.setTitle("tap_to_open".localized, for: .normal)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
animator?.stop()
}
// MARK: - Actions
@IBAction private func openAppTapped(_ sender: Any) {
extensionContext?.performNotificationDefaultAction()
}
}
extension NotificationContentViewController: UNNotificationContentExtension {
func didReceive(_ notification: UNNotification) {
let content = notification.request.content
titleLabel.text = content.title
descriptionLabel.text = content.body
let notification = EQNRealtimePushNotification.from(
userInfo: content.userInfo,
title: "",
displayTitle: content.title,
displayBody: content.body
)
if let notification {
let coordinate = notification.coordinate.coordinate
let span = MKCoordinateSpan(latitudeDelta: 6, longitudeDelta: 6)
let region = MKCoordinateRegion(
center: coordinate,
span: span
)
mapView.setCenter(coordinate, animated: false)
mapView.setRegion(region, animated: true)
switch notification.type.lowercased() {
case "eqn":
let annotation = EQNMapAnnotationPastquake(
title: "",
coordinate: coordinate,
intensity: notification.intensity
)
mapView.addAnnotation(annotation)
case "manual":
let annotation = EQNMapAnnotationUserReport(
magnitude: notification.magnitude,
coordinate: coordinate
)
mapView.addAnnotation(annotation)
default:
break
}
// create animator to manage wave animation and countdown
animator = MapSeismicWaveAnimator(
realtimeAlert: notification,
mapView: mapView,
waveTimeLabel: waveLabel
)
animator?.start()
// set color based on intensity
descriptionLabel.textColor = notification.relativeIntensityColor
}
}
}
extension NotificationContentViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: any MKAnnotation) -> MKAnnotationView? {
switch annotation {
case let pastquake as EQNMapAnnotationPastquake:
let annotationView = mapView.dequeueReusableAnnotationView(
withIdentifier: EQNCustomAnnotationView.SingleLineIdentifier
) as! EQNCustomAnnotationView
annotationView.image = pastquake.image
annotationView.title = pastquake.title
return annotationView
case let report as EQNMapAnnotationUserReport:
let annotationView = mapView.dequeueReusableAnnotationView(
withIdentifier: EQNCustomAnnotationView.SmallIdentifier
) as! EQNCustomAnnotationView
annotationView.image = report.image(with: EQNCustomAnnotationView.SmallViewImageHeight)
annotationView.title = report.timeDifference
return annotationView
default:
return nil
}
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
animator?.getOverlayRenderer(for: overlay) ?? MKOverlayRenderer(overlay: overlay)
}
}
@@ -1,13 +0,0 @@
//
// NotificationViewController.h
// EQNNotificationContent
//
// Refactored by Andrea Busi on 14/10/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface NotificationViewController : UIViewController
@end
@@ -1,129 +0,0 @@
//
// NotificationViewController.m
// EQNNotificationContent
//
// Refactored by Andrea Busi on 14/10/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "NotificationViewController.h"
#import "EQNUtility.h"
#import "EQNNotificationContent-Swift.h"
@import UserNotifications;
@import UserNotificationsUI;
@import MapKit;
@interface NotificationViewController () <UNNotificationContentExtension, MKMapViewDelegate>
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UILabel *descriptionLabel;
@property (weak, nonatomic) IBOutlet UILabel *waveLabel;
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
/// This will be calculated as seismic date + warning time
@property (strong, nonatomic) NSDate *userSeismicTimestamp;
@property (strong, nonatomic) NSTimer *countdownTimer;
@end
@implementation NotificationViewController
- (void)setMapView:(MKMapView *)mapView
{
_mapView = mapView;
_mapView.scrollEnabled = NO;
_mapView.zoomEnabled = NO;
}
#pragma mark - View Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self.mapView registerClass:[EQNCustomAnnotationView class] forAnnotationViewWithReuseIdentifier:EQNCustomAnnotationView.SingleLineIdentifier];
[self.mapView registerClass:[EQNCustomAnnotationView class] forAnnotationViewWithReuseIdentifier:EQNCustomAnnotationView.SmallIdentifier];
}
- (void)didReceiveNotification:(UNNotification *)notification
{
UNNotificationContent *content = notification.request.content;
NSDictionary *userInfo = content.userInfo;
// set title and description
self.titleLabel.text = content.title;
self.descriptionLabel.text = content.body;
// add annotation onthe map
CLLocation *coordinate = [[CLLocation alloc] initWithLatitude:[userInfo[@"latitude"] doubleValue]
longitude:[userInfo[@"longitude"] doubleValue]];
MKCoordinateSpan span = MKCoordinateSpanMake(10.5, 10.5);
MKCoordinateRegion region = MKCoordinateRegionMake(coordinate.coordinate, span);
[self.mapView setCenterCoordinate:coordinate.coordinate animated:NO];
[self.mapView setRegion:region animated:YES];
if ([userInfo[@"type"] isEqualToString:@"eqn"]) {
EQNMapAnnotationPastquake *annotation = [[EQNMapAnnotationPastquake alloc] initWithTitle:@""
coordinate:coordinate.coordinate
intensity:[userInfo[@"intensity"] intValue]];
[self.mapView addAnnotation:annotation];
} else if ([userInfo[@"type"] isEqualToString:@"manual"]){
EQNMapAnnotationUserReport *annotation = [[EQNMapAnnotationUserReport alloc] initWithMagnitude:[userInfo[@"magnitude"] intValue]
coordinate:coordinate.coordinate];
[self.mapView addAnnotation:annotation];
}
self.userSeismicTimestamp = [EQNUtility calculateUserSeismicTimestampFromUserInfo:userInfo];
if (self.userSeismicTimestamp) {
// start the countdown
self.countdownTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(countdownFired:)
userInfo:nil
repeats:YES];
[self.countdownTimer fire];
}
}
#pragma mark - Private
- (void)countdownFired:(id)sender
{
NSDate *now = [NSDate date];
NSTimeInterval difference = MAX([self.userSeismicTimestamp timeIntervalSinceDate:now], 0);
NSInteger seconds = (int)lround(difference);
self.waveLabel.text = [NSString localizedStringWithFormat:NSLocalizedString(@"alert_wave", @""), seconds];
if (difference <= 0) {
// stop the countdown
[self.countdownTimer invalidate];
self.countdownTimer = nil;
}
}
#pragma mark - MKMapViewDelegate
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[EQNMapAnnotationPastquake class]]) {
EQNMapAnnotationPastquake *pastquake = (EQNMapAnnotationPastquake *)annotation;
EQNCustomAnnotationView *annotationView = (EQNCustomAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:EQNCustomAnnotationView.SingleLineIdentifier];
annotationView.image = pastquake.image;
annotationView.title = pastquake.title;
return annotationView;
} else if ([annotation isKindOfClass:[EQNMapAnnotationUserReport class]]) {
EQNMapAnnotationUserReport *report = (EQNMapAnnotationUserReport *)annotation;
EQNCustomAnnotationView *annotationView = (EQNCustomAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:EQNCustomAnnotationView.SmallIdentifier];
annotationView.image = [report imageWithHeight:EQNCustomAnnotationView.SmallViewImageHeight];
annotationView.title = report.timeDifference;
return annotationView;
}
return nil;
}
@end
@@ -51,6 +51,43 @@ class NotificationService: UNNotificationServiceExtension {
bestAttemptContent.sound = UNNotificationSound(named: Self.EQNSoundNotification)
}
// evaluate intensity and get proper string to display
guard let latitude = userInfo.double(forKey: "latitude"),
let longitude = userInfo.double(forKey: "longitude"),
let peak = userInfo.double(forKey: "peak") else {
print("[NotificationService] Unable to get base info for intensity calculation")
return
}
let magnitude = userInfo.double(forKey: "mag") ?? 0
let location = CLLocation(latitude: latitude, longitude: longitude)
guard let distance = EQNUserData.shared.lastLocation?.distance(from: location) else {
print("[NotificationService] Unable to calculate distance or get last location")
return
}
let distanceKm = distance / 1_000
// If the shake is mild, user can disale sound and use default notification sound
// This logic is done here and not with the rest because distance and other informations are needed
let mildQuakeSoundDisabled = UserDefaults.appGroup?.bool(forKey: UserDefaults.AllertaSismicaSuonoDisabilitatoSismaDebole) ?? true
let isMildQuake = isMildQuake(magnitude: magnitude, distance: distanceKm)
if isMildQuake && mildQuakeSoundDisabled {
bestAttemptContent.sound = UNNotificationSound.default
}
let intensita = peak * exp(-distanceKm/peak/250)
let stringSuffix = if intensita < 0.004 {
"no_shaking"
} else if intensita < 0.30 {
"mild"
} else if intensita < 0.70 {
"moderate"
} else {
"strong"
}
bestAttemptContent.body = "alert_intensity_\(stringSuffix)".localized
let intensity = userInfo.integer(forKey: "intensity")
switch intensity {
case 0:
@@ -63,57 +100,13 @@ class NotificationService: UNNotificationServiceExtension {
break
}
// evaluate intensity and get proper string to display
guard let latitude = userInfo.double(forKey: "latitude"),
let longitude = userInfo.double(forKey: "longitude"),
let peak = userInfo.double(forKey: "peak") else {
print("[NotificationService] Unable to get base info for intensity calculation")
return
}
let location = CLLocation(latitude: latitude, longitude: longitude)
guard let distance = EQNUserData.shared.lastLocation?.distance(from: location) else {
print("[NotificationService] Unable to calculate distance or get last location")
return
}
let distanceKm = distance / 1_000
let intensita = peak * exp(-distanceKm/peak/250)
let stringSuffix: String
if intensita < 0.004 {
stringSuffix = "no_shaking"
} else if intensita < 0.30 {
stringSuffix = "mild"
} else if intensita < 0.70 {
stringSuffix = "moderate"
} else {
stringSuffix = "strong"
}
bestAttemptContent.body = "alert_intensity_\(stringSuffix)".localized
case "manual":
// there are 12 levels, so a customized icon doesn't make sense
// use a generic warning icon instead
iconName = "warning_yellow.png"
case "official":
let provider = userInfo.string(forKey: "provider", orDefault: "")
let intensity = userInfo.double(forKey: "magnitude", orDefault: 0)
let color: String
if intensity < 2.0 {
color = "_white"
} else if intensity < 3.5 {
color = "_green"
} else if intensity < 4.5 {
color = "_yellow"
} else if intensity < 5.5 {
color = "_red"
} else {
color = "_purple"
}
iconName = manualIconName(for: provider, color: color)
// don't show any images
break
default:
break
}
@@ -174,7 +167,7 @@ class NotificationService: UNNotificationServiceExtension {
notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers)
// !! Note: this is a known issue/bug
// we need to add a delay before invoking the completion, otherwise the notification will not be remved
// we need to add a delay before invoking the completion, otherwise the notification will not be removed
// ref: https://stackoverflow.com/questions/53697279/why-are-notifications-not-removed-with-removedeliverednotifications
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
completion()
@@ -182,6 +175,20 @@ class NotificationService: UNNotificationServiceExtension {
}
}
private func isMildQuake(
magnitude: Double,
distance: Double
) -> Bool {
var intensity_at_location: Double = 0
if distance > 0 {
let R: Double = 6371
let eq_depth: Double = 10.0
let hyp_distance = sqrt(pow(eq_depth, 2) + 4 * R * (R - eq_depth) * pow(sin(distance / (2 * R)), 2))
intensity_at_location = -2.15 * log10(hyp_distance) + 1.0 * magnitude + 2.31
}
return intensity_at_location < 3
}
// MARK: - Helpers
private func manualIconName(for provider: String, color: String) -> String {
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,186 @@
{
"originHash" : "898a30d298491e1ce821191ebfa2e7d28e7c9ea4c119cbfdfb1b245bc94ac6c3",
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.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" : "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5",
"version" : "11.15.0"
}
},
{
"identity" : "google-ads-on-device-conversion-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
"state" : {
"revision" : "428d8bb138e00f9a3f4f61cc6cd8863607524f65",
"version" : "2.1.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "45ce435e9406d3c674dd249a042b932bee006f60",
"version" : "11.15.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" : "60da361632d0de02786f709bdc0c4df340f7613e",
"version" : "8.1.0"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71",
"version" : "1.69.0"
}
},
{
"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" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.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.git",
"state" : {
"revision" : "809b56a43fadac72db9963a21c74688af7ef51b7",
"version" : "2.1.0"
}
},
{
"identity" : "solar",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ceeK/Solar.git",
"state" : {
"revision" : "c2b96f2d5fb7f835b91cefac5e83101f54643901",
"version" : "3.0.1"
}
},
{
"identity" : "swift-package-manager-google-mobile-ads",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/swift-package-manager-google-mobile-ads.git",
"state" : {
"revision" : "aa24c7dc03bca62c42747314dc8537f15587b50d",
"version" : "12.0.0"
}
},
{
"identity" : "swift-package-manager-google-user-messaging-platform",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/swift-package-manager-google-user-messaging-platform.git",
"state" : {
"revision" : "9b68aa69fb508f0274853e226c734151a973c7b7",
"version" : "2.4.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "9f0c76544701845ad98716f3f6a774a892152bcb",
"version" : "1.26.0"
}
}
],
"version" : 3
}
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1410"
LastUpgradeVersion = "1620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1410"
LastUpgradeVersion = "1620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
@@ -15,7 +15,7 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8C4B0B7921CACE3F00AED489"
BlueprintIdentifier = "65FFDC91292F672B00EA821B"
BuildableName = "EQNNotificationService.appex"
BlueprintName = "EQNNotificationService"
ReferencedContainer = "container:Earthquake Network.xcodeproj">
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1410"
LastUpgradeVersion = "1620"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Earthquake Network.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
@@ -1,14 +0,0 @@
{
"pins" : [
{
"identity" : "shogun",
"kind" : "remoteSourceControl",
"location" : "https://github.com/andreabusi-it/Shogun",
"state" : {
"revision" : "3ffa7cfbdcbfb9868c853900f63d7e3248db797e",
"version" : "1.1.1"
}
}
],
"version" : 2
}
+32 -47
View File
@@ -8,15 +8,9 @@
#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"
@@ -56,6 +50,9 @@
[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];
@@ -63,7 +60,6 @@
[self configurePushNotifications];
[application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
[application registerForRemoteNotifications];
return YES;
@@ -84,8 +80,10 @@
- (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];
// 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;
@@ -151,7 +149,7 @@
[self handlePushNotificationWithNotificationContent:content];
// Change this to your preferred presentation option
completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound);
completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionSound);
}
// Handle notification messages after display notification is tapped by the user.
@@ -166,36 +164,28 @@
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)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 ([type isEqualToString:@"eqn"]) {
// Store both original payload and modified title/body
// This will be usefull to avoid to re-evaluate logic for title display.
NSDictionary *notification = @{
@"title": content.title,
@"body": content.body,
@"userInfo": content.userInfo
};
if ([type isEqualToString:@"eqn"]) {
[EQNRealtimePushNotification storeNotificationWithPayload:notification];
section = EQNTabBarSectionAllerte;
} else if([type isEqualToString:@"manual"]) {
section = EQNTabBarSectionSegnalazioni;
} else if([type isEqualToString:@"official"]) {
[EQNOfficialPushNotification storeNotificationWithPayload:notification];
section = EQNTabBarSectionRetiSismiche;
}
@@ -217,20 +207,16 @@
- (void)configureAppTracking
{
if (@available(iOS 14, *)) {
// add a delay otherwise the alert will not be displayed
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
if (status == ATTrackingManagerAuthorizationStatusAuthorized) {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = YES;
} else {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = NO;
}
}];
});
} else {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = YES;
}
// add a delay otherwise the alert will not be displayed
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
if (status == ATTrackingManagerAuthorizationStatusAuthorized) {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = YES;
} else {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = NO;
}
}];
});
}
- (void)configureFirebase
@@ -242,8 +228,7 @@
- (void)configureFacebookSDKWithApplication:(UIApplication *)application andOptions:(NSDictionary *)launchOptions
{
[FBSDKApplicationDelegate.sharedInstance application:application didFinishLaunchingWithOptions:launchOptions];
[FBSDKSettings.sharedSettings setIsAdvertiserIDCollectionEnabled:YES];
[FBSDKSettings.sharedSettings setIsAdvertiserIDCollectionEnabled:YES];
FBSDKSettings.sharedSettings.isAdvertiserIDCollectionEnabled = YES;
}
#pragma mark - FIRMessagingDelegate
@@ -253,9 +238,9 @@
NSLog(@"[Firebase] fcmToken %@", fcmToken);
if (EQNUserData.sharedData.isFirstStart) {
// save default values for notification settings
[EQNAllertaSismica saveDefaultValues];
[EQNNotificheSegnalazioniUtente saveDefaultValues];
[EQNNotificheReteSismiche saveDefaultValues];
[EQNSettingRealTimeAlert saveDefaultValues];
[EQNSettingUserReportNotification saveDefaultValues];
[EQNSettingSeismicNetworkNotification saveDefaultValues];
}
[EQNUser.defaultUser registerUserIfNeededWithFirebaseToken:fcmToken];
+57 -12
View File
@@ -17,24 +17,17 @@ extension UserDefaults {
// Impostazioni della sezione `Allerta in tempo reale`
static let AllertaSismicaAbilitato = "NOTIFICHE_ALLERA_SISMICA_ABILITATO"
static let AllertaSismicaSuonoDisabilitatoSismaDebole = "NOTIFICHE_ALLERA_SISMICA_SUONO_DISABILITATO_SISMA_DEBOLE"
static let AllertaSismicaCriticalAlerts = "NOTIFICHE_ALLERA_SISMICA_CRITICAL_ALERTS"
static let AllertaSismicaSismiDaNotificare = "NOTIFICHE_ALLERA_SISMICA_SISMI_DA_NOTIFICARE"
static let AllertaSismicaRaggioSismiLievi = "NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_LIEVI"
static let AllertaSismicaRaggioSismiForti = "NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_FORTI"
static let AllertaSismicaImpostaVolume = "NOTIFICHE_ALLERA_SISMICA_IMPOSTA_VOLUME"
static let AllertaSismicaTestaAllarma = "NOTIFICHE_ALLERA_SISMICA_TESTA_ALLARME"
static let AllertaSismicaAbilitaIntervallo = "NOTIFICHE_ALLERA_SISMICA_ABILITA_INTERVALLO"
static let AllertaSismicaOraInizio = "NOTIFICHE_ALLERA_SISMICA_ORA_INIZIO"
static let AllertaSismicaOraFine = "NOTIFICHE_ALLERA_SISMICA_ORA_INIZIO"
// Impostazioni della sezione `Notifiche da reti sismiche`
static let NotificheRetiSismicheAbilitato = "NOTIFICHE_ATTIVA_RETI_SISMICHE"
static let NotificheRetiSismicheViciniAbilitato = "NOTIFICHE_ATTIVA_RETI_SISMICHE_VICINE"
static let NotificheRetiSismicheTerremotiFortiAbilitato = "NOTIFICHE_ATTIVA_RETI_TERREMOTI_FORTI"
static let NotificheRetiSismicheDistanzaPosizione = "NOTIFICHE_DISTANZA_POSIZIONE_RETI_SISMICHE"
static let NotificheRetiSismicheEnergiaSisma = "NOTIFICHE_ATTIVA_RETI_ENERGIA_SISMI"
static let NotificheRetiSismicheEnergiaTerremotiForti = "NOTIFICHE_ATTIVA_RETI_ENERGIA_FORTI"
static let NotificheRetiSismicheListaEnti = "NOTIFICHE_ATTIVA_RETI_LISTA_ENTI"
static let NotificheRetiSismicheMagnitudoMinima = "NOTIFICHE_ATTIVA_RETI_ENERGIA_SISMI"
static let NotificheRetiSismicheDistanzaMassima = "NOTIFICHE_DISTANZA_POSIZIONE_RETI_SISMICHE"
static let NotificheRetiSismicheFiltroNotifiche = "NOTIFICHE_FILTRO_NOTIFICHE_RETI_SISMICHE"
// Impostazioni della sezione `Notifiche segnalazioni utente`
static let NotificheSegnalazioniUtenteAbilitato = "NOTIFICHE_SU_ATTIVA_SEGNALAZIONE_UTENTE"
@@ -45,6 +38,7 @@ extension UserDefaults {
static let UserReportCodeStatus = "CODE_MESSAGE_EQN"
// Proprietà e preferenze dell'utente
static let FirstAppStartExecuted = "EQNUserDefaultFirstAppStartExecuted"
/// Ultima posizione conosciuta dell'utente
static let UserDataLastLocation = "EQNLast_Location"
/// Token Firebase dell'utente corrente
@@ -59,12 +53,63 @@ extension UserDefaults {
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 AppMigrationV5_10 = "EQNUserDefaultMigrationV5_10"
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,8 +26,6 @@
@implementation AllerteViewController
static NSString * const SegueIdentifierPrioritySubscriptions = @"ShowPrioritySubscriptions";
/// Sections inside the app
typedef NS_ENUM(NSInteger, AllerteTableRow) {
AllerteTableRowLocationPermission = 0,
@@ -102,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
@@ -122,9 +133,7 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
// mostriamo la schermata solo se il countdown non è a zero
if (![notification isCountdownExpired]) {
RealtimeAlertViewController *controller = [[RealtimeAlertViewController alloc] initWithNotification:notification];
if (@available(iOS 13.0, *)) {
controller.modalInPresentation = YES;
}
controller.modalInPresentation = YES;
[self presentViewController:controller animated:YES completion:nil];
}
} else {
@@ -140,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)];
}
@@ -177,6 +186,13 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
[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
{
[self resetRealtimeAlert];
@@ -219,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)
@@ -256,14 +272,16 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
if (tableRow == AllerteTableRowLocationPermission) {
AlertsNoLocationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"NoLocationCell" forIndexPath:indexPath];
cell.status = CLLocationManager.authorizationStatus;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell updateWith:EQNUserData.sharedData.locationAuthorizationStatus];
return cell;
} else if (tableRow == AllerteTableRowSismiRilevati) {
if (self.isNotificaAttiva) {
AlertsSeismicNotificationExpandedTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SeismicNotificationExpandedCell" forIndexPath:indexPath];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
EQNRealtimePushNotification *notification = [EQNRealtimePushNotification storedNotification];
cell.notification = notification;
[cell updateWith:notification];
__weak AllerteViewController *weakSelf = self;
cell.onTapClose = ^{
@@ -282,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 = ^{
@@ -302,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];
};
@@ -311,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];
};
@@ -319,12 +340,13 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
} else if (tableRow == AllerteTableRowServizioPriorita) {
AlertsPriorityServiceTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PriorityCell" forIndexPath:indexPath];
cell.smartphoneNetwork = [EQNManager defaultManager].rete_smartphone;
[cell updateWith:[EQNManager defaultManager].rete_smartphone];
return cell;
} else if (tableRow == AllerteTableRowDatiPosizione) {
AlertsPositionDataTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PositionDataCell" forIndexPath:indexPath];
cell.position = [EQNUser defaultUser].lastPosition;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell updateWith:[EQNUser defaultUser].lastPosition];
return cell;
}
@@ -337,9 +359,10 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
AllerteTableRow tableRow = [self.tableItems[indexPath.row] integerValue];
switch (tableRow) {
case AllerteTableRowServizioPriorita:
[self performSegueWithIdentifier:SegueIdentifierPrioritySubscriptions sender:nil];
break;
case AllerteTableRowServizioPriorita: {
SubscriptionsViewController *controller = [[SubscriptionsViewController alloc] init];
[self.navigationController pushViewController:controller animated:YES];
}; break;
default:
break;
}
@@ -9,41 +9,70 @@
import UIKit
import CoreLocation
class AlertsNoLocationTableViewCell: EQNBaseTableViewCell {
@objc var status: CLAuthorizationStatus = .notDetermined {
didSet {
updateUI()
}
@objc
class AlertsNoLocationTableViewCell: EQNBaseContainerTableViewCell {
override var isHeaderVisible: Bool { false }
// MARK: - UI
private lazy var messageLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.Colors.red
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private lazy var actionButton: UIButton = {
let button = EQNRoundedButton.make(title: NSLocalizedString("permission_location_no_background_solve", comment: ""), target: self, action: #selector(solveTapped(_:)))
return button
}()
// MARK: - Internal
override func setupUI() {
super.setupUI()
containerView.addSubview(messageLabel)
containerView.addSubview(actionButton)
messageLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
messageLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
messageLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
actionButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
actionButton.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: .cardPadding).isActive = true
actionButton.leadingAnchor.constraint(equalTo: messageLabel.leadingAnchor).isActive = true
actionButton.trailingAnchor.constraint(equalTo: messageLabel.trailingAnchor).isActive = true
actionButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
@IBOutlet private weak var messageLabel: UILabel!
@IBOutlet private weak var actionButton: UIButton!
override func updateUI() {
super.updateUI()
actionButton.backgroundColor = AppTheme.Colors.lightGray
}
// MARK: - Private
// MARK: - Public
private func updateUI() {
var message = ""
switch status {
case .authorizedAlways:
message = ""
case .authorizedWhenInUse:
message = NSLocalizedString("permission_location_no_background", comment: "")
default:
message = NSLocalizedString("permission_location_no", comment: "")
@objc
func update(with status: CLAuthorizationStatus) {
messageLabel.text = switch status {
case .authorizedAlways: ""
case .authorizedWhenInUse: NSLocalizedString("permission_location_no_background", comment: "")
default: NSLocalizedString("permission_location_no", comment: "")
}
messageLabel.text = message
actionButton.setLocalizedTitle(key: "permission_location_no_background_solve")
}
// MARK: - Actions
@IBAction private func solveTapped(_ sender: UIButton) {
@objc private func solveTapped(_ sender: UIButton) {
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
return
}
UIApplication.shared.open(settingsUrl, options: [:], completionHandler: nil)
}
}
@@ -8,50 +8,87 @@
import UIKit
class AlertsPastEartquakesTableViewCell: EQNBaseTableViewCell {
@objc var smartphoneNetwork: EQNReteSmartphone? {
didSet {
updateUI()
}
}
@objc
class AlertsPastEartquakesTableViewCell: EQNBaseContainerTableViewCell {
@objc var onTapMapButton: (() -> Void)?
@objc var onTapMap: (() -> Void)?
override var headerText: String { NSLocalizedString("main_past_quakes", comment: "") }
// MARK: - UI
private lazy var last24hLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .title3)
label.textAlignment = .center
return label
}()
private lazy var from2013Label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .title3)
label.textAlignment = .center
return label
}()
private lazy var mapButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(mapTapped(_:)))
return button
}()
// MARK: - Internal
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var last24hLabel: UILabel!
@IBOutlet private weak var from2013Label: UILabel!
@IBOutlet private weak var mapButton: UIButton!
// MARK: - View Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(last24hLabel)
containerView.addSubview(from2013Label)
containerView.addSubview(mapButton)
last24hLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
last24hLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
last24hLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
from2013Label.topAnchor.constraint(equalTo: last24hLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
from2013Label.leadingAnchor.constraint(equalTo: last24hLabel.leadingAnchor).isActive = true
from2013Label.trailingAnchor.constraint(equalTo: last24hLabel.trailingAnchor).isActive = true
mapButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
mapButton.topAnchor.constraint(equalTo: from2013Label.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
mapButton.leadingAnchor.constraint(equalTo: from2013Label.leadingAnchor).isActive = true
mapButton.trailingAnchor.constraint(equalTo: from2013Label.trailingAnchor).isActive = true
mapButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("main_past_quakes", comment: "")
override func updateUI() {
super.updateUI()
last24hLabel.text = NSLocalizedString("main_recent_quakes_initial", comment: "")
from2013Label.text = NSLocalizedString("main_total_quakes_initial", comment: "")
mapButton.setLocalizedTitle(key: "official_button_map", uppercased: true, emoji: "🗺")
}
private func updateUI() {
guard let smartphoneNetwork = smartphoneNetwork else { return }
// MARK: - Public
@objc
func update(with smartphoneNetwork: EQNReteSmartphone?) {
guard let smartphoneNetwork else { return }
last24hLabel.text = String(format: NSLocalizedString("main_recent_quakes", comment: ""), smartphoneNetwork.counterLastDayAlerts)
from2013Label.text = String(format: NSLocalizedString("main_total_quakes", comment: ""), smartphoneNetwork.counterTotalAlerts)
}
// MARK: - Actions
@IBAction func mapTapped(_ sender: UIButton) {
onTapMapButton?()
@objc private func mapTapped(_ sender: UIButton) {
onTapMap?()
}
}
@@ -9,20 +9,11 @@
import UIKit
import Solar
class AlertsPositionDataTableViewCell: EQNBaseTableViewCell {
@objc var position: CLLocation? {
didSet {
updateUI()
}
}
@objc
class AlertsPositionDataTableViewCell: EQNBaseContainerTableViewCell {
// MARK: - Internal
override var headerText: String { NSLocalizedString("weather_location", comment: "") }
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var positionLabel: UILabel!
@IBOutlet private weak var sunriseTimeLabel: UILabel!
@IBOutlet private weak var sunsetTimeLabel: UILabel!
private lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
@@ -30,26 +21,117 @@ class AlertsPositionDataTableViewCell: EQNBaseTableViewCell {
return formatter
}()
// MARK: - Private
// MARK: - UI
private func updateUI() {
headerLabel.text = NSLocalizedString("weather_location", comment: "")
private lazy var positionImage: UIImageView = {
let imageView = UIImageView(image: .init(named: "world_old"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
return imageView
}()
private lazy var positionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private lazy var sunriseImage: UIImageView = {
let imageView = UIImageView(image: .init(named: "sunrise"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
return imageView
}()
private lazy var sunriseTimeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private lazy var sunsetImage: UIImageView = {
let imageView = UIImageView(image: .init(named: "sunset"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
return imageView
}()
private lazy var sunsetTimeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
return label
}()
// MARK: - Internal
override func setupUI() {
super.setupUI()
containerView.addSubview(positionImage)
containerView.addSubview(positionLabel)
containerView.addSubview(sunriseImage)
containerView.addSubview(sunriseTimeLabel)
containerView.addSubview(sunsetImage)
containerView.addSubview(sunsetTimeLabel)
positionImage.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
positionImage.centerYAnchor.constraint(equalTo: positionLabel.centerYAnchor).isActive = true
positionImage.trailingAnchor.constraint(equalTo: positionLabel.leadingAnchor, constant: .cardPadding.negative).isActive = true
positionLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
positionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
sunriseImage.leadingAnchor.constraint(equalTo: positionImage.leadingAnchor).isActive = true
sunriseImage.centerYAnchor.constraint(equalTo: sunriseTimeLabel.centerYAnchor).isActive = true
sunriseImage.trailingAnchor.constraint(equalTo: sunriseTimeLabel.leadingAnchor, constant: .cardPadding.negative).isActive = true
sunriseTimeLabel.topAnchor.constraint(equalTo: positionLabel.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
sunriseTimeLabel.trailingAnchor.constraint(equalTo: positionLabel.trailingAnchor).isActive = true
sunsetImage.leadingAnchor.constraint(equalTo: sunriseImage.leadingAnchor).isActive = true
sunsetImage.centerYAnchor.constraint(equalTo: sunsetTimeLabel.centerYAnchor).isActive = true
sunsetImage.trailingAnchor.constraint(equalTo: sunsetTimeLabel.leadingAnchor, constant: .cardPadding.negative).isActive = true
sunsetTimeLabel.topAnchor.constraint(equalTo: sunriseTimeLabel.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
sunsetTimeLabel.trailingAnchor.constraint(equalTo: sunriseTimeLabel.trailingAnchor).isActive = true
sunsetTimeLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
override func updateUI() {
super.updateUI()
positionLabel.text = "n.d."
sunriseTimeLabel.text = "n.d."
sunsetTimeLabel.text = "n.d."
guard let position = position else { return }
}
// MARK: - Public
@objc
func update(with position: CLLocation?) {
guard let position else { return }
positionLabel.text = EQNUtility.coordinateString(coordinate: position.coordinate)
if let solar = Solar(coordinate: position.coordinate) {
let timeZone = TimeZone.current.localizedName(for: .generic, locale: .current) ?? TimeZone.current.identifier
if let sunrise = solar.sunrise {
sunriseTimeLabel.text = dateFormatter.string(from: sunrise) + " \(timeZone)"
}
if let sunset = solar.sunset {
sunsetTimeLabel.text = dateFormatter.string(from: sunset) + " \(timeZone)"
}
guard let solar = Solar(coordinate: position.coordinate) else { return }
let timeZone = TimeZone.current.localizedName(for: .generic, locale: .current) ?? TimeZone.current.identifier
if let sunrise = solar.sunrise {
sunriseTimeLabel.text = dateFormatter.string(from: sunrise) + " \(timeZone)"
}
if let sunset = solar.sunset {
sunsetTimeLabel.text = dateFormatter.string(from: sunset) + " \(timeZone)"
}
}
}
@@ -8,37 +8,62 @@
import UIKit
class AlertsPriorityServiceTableViewCell: EQNBaseTableViewCell {
@objc var smartphoneNetwork: EQNReteSmartphone? {
didSet {
updateUI()
}
}
@objc
class AlertsPriorityServiceTableViewCell: EQNBaseContainerTableViewCell {
override var headerText: String { NSLocalizedString("inapp_list", comment: "") }
override var isRightArrowVisbile: Bool { true }
// MARK: - UI
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.Colors.darkGray
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private lazy var lastSubscriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.Colors.pureRed
label.font = .preferredFont(forTextStyle: .body)
return label
}()
// MARK: - Internal
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var descriptionLabel: UILabel!
@IBOutlet private weak var lastSubscriptionLabel: UILabel!
// MARK: - View Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(descriptionLabel)
containerView.addSubview(lastSubscriptionLabel)
descriptionLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
lastSubscriptionLabel.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: .cardVerticalSpacing/2.0).isActive = true
lastSubscriptionLabel.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor).isActive = true
lastSubscriptionLabel.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
lastSubscriptionLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("inapp_list", comment: "")
override func updateUI() {
super.updateUI()
backgroundColor = AppTheme.Colors.cardBackgroundOrange
descriptionLabel.text = NSLocalizedString("inapp_adv", comment: "")
}
private func updateUI() {
guard let smartphoneNetwork = smartphoneNetwork else { return }
// MARK: - Public
@objc
func update(with smartphoneNetwork: EQNReteSmartphone?) {
guard let smartphoneNetwork else { return }
lastSubscriptionLabel.text = subscriptionText(for: smartphoneNetwork.lastSubscriptionDiff)
}
@@ -9,54 +9,112 @@
import UIKit
class AlertsSeismicNotificationCompactTableViewCell: EQNBaseTableViewCell {
@objc
class AlertsSeismicNotificationCompactTableViewCell: EQNBaseContainerTableViewCell {
typealias DefaultCompletion = () -> Void
@objc var onTapAlertTest: DefaultCompletion?
@objc var onTapSimulator: DefaultCompletion?
@objc var onTapHowItWorks: DefaultCompletion?
@objc var onTapShareApp: DefaultCompletion?
override var isHeaderVisible: Bool { false }
// MARK: - UI
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.Colors.green
label.font = .preferredFont(forTextStyle: .title3)
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
@IBOutlet private weak var descriptionLabel: UILabel!
@IBOutlet private weak var testAlertButton: UIButton!
@IBOutlet private weak var simulatorAlertButton: UIButton!
@IBOutlet private weak var howItWorksAlertButton: UIButton!
@IBOutlet private weak var shareAppButton: UIButton!
private lazy var testAlertButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(testAlertTapped(_:)))
return button
}()
// MARK: - View Lifecycle
private lazy var simulatorAlertButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(simulatorTapped(_:)))
return button
}()
override func awakeFromNib() {
super.awakeFromNib()
private lazy var howItWorksAlertButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(howItWorksTapped(_:)))
return button
}()
private lazy var shareAppButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(shareAppTapped(_:)))
return button
}()
// MARK: - Internal
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(descriptionLabel)
containerView.addSubview(testAlertButton)
containerView.addSubview(simulatorAlertButton)
containerView.addSubview(howItWorksAlertButton)
containerView.addSubview(shareAppButton)
descriptionLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardPadding).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
testAlertButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
simulatorAlertButton.heightAnchor.constraint(equalTo: testAlertButton.heightAnchor).isActive = true
howItWorksAlertButton.heightAnchor.constraint(equalTo: testAlertButton.heightAnchor).isActive = true
shareAppButton.heightAnchor.constraint(equalTo: testAlertButton.heightAnchor).isActive = true
testAlertButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
testAlertButton.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor).isActive = true
testAlertButton.trailingAnchor.constraint(equalTo: simulatorAlertButton.leadingAnchor, constant: .cardPadding.negative).isActive = true
simulatorAlertButton.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
simulatorAlertButton.centerYAnchor.constraint(equalTo: testAlertButton.centerYAnchor).isActive = true
simulatorAlertButton.widthAnchor.constraint(equalTo: testAlertButton.widthAnchor).isActive = true
howItWorksAlertButton.topAnchor.constraint(equalTo: testAlertButton.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
howItWorksAlertButton.leadingAnchor.constraint(equalTo: testAlertButton.leadingAnchor).isActive = true
howItWorksAlertButton.trailingAnchor.constraint(equalTo: shareAppButton.leadingAnchor, constant: .cardPadding.negative).isActive = true
shareAppButton.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
shareAppButton.centerYAnchor.constraint(equalTo: howItWorksAlertButton.centerYAnchor).isActive = true
shareAppButton.widthAnchor.constraint(equalTo: howItWorksAlertButton.widthAnchor).isActive = true
shareAppButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
override func updateUI() {
super.updateUI()
backgroundColor = AppTheme.Colors.cardBackgroundGreen
descriptionLabel.text = NSLocalizedString("main_nodetection", comment: "")
testAlertButton.setLocalizedTitle(key: "main_alerttest", uppercased: true, emoji: "🚨")
simulatorAlertButton.setLocalizedTitle(key: "main_simulator", uppercased: true, emoji: "")
howItWorksAlertButton.setLocalizedTitle(key: "main_how_it_work", uppercased: true, emoji: "💡")
shareAppButton.setLocalizedTitle(key: "main_share_app", uppercased: true, emoji: "👥")
}
// MARK: - Actions
@IBAction private func testAlertTapped() {
@objc private func testAlertTapped(_ sender: UIButton) {
onTapAlertTest?()
}
@IBAction private func simulatorTapped() {
@objc private func simulatorTapped(_ sender: UIButton) {
onTapSimulator?()
}
@IBAction private func howItWorksTapped() {
@objc private func howItWorksTapped(_ sender: UIButton) {
onTapHowItWorks?()
}
@IBAction private func shareAppTapped() {
@objc private func shareAppTapped(_ sender: UIButton) {
onTapShareApp?()
}
}
@@ -10,78 +10,173 @@ import UIKit
import MapKit
import Shogun
class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMapViewDelegate {
class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseContainerTableViewCell, MKMapViewDelegate {
override var isHeaderVisible: Bool { false }
typealias DefaultCompletion = () -> Void
@objc var notification: EQNRealtimePushNotification? {
didSet {
updateUI()
}
}
@objc var onTapOpenTwitter: DefaultCompletion?
@objc var onTapRateApp: DefaultCompletion?
@objc var onTapClose: DefaultCompletion?
@objc var onTapShareApp: DefaultCompletion?
// MARK: - UI
private lazy var notificationTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .title1)
label.textAlignment = .center
return label
}()
private lazy var notificationIntensityLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .title1)
label.textAlignment = .center
return label
}()
private lazy var notificationDescriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
return label
}()
private lazy var mapView: MKMapView = {
let mapView = MKMapView()
mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.delegate = self
mapView.isScrollEnabled = false
mapView.isZoomEnabled = false
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
return mapView
}()
private lazy var shareButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(shareAppTapped(_:)))
return button
}()
private lazy var rateAppButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(rateAppTapped(_:)))
return button
}()
private lazy var viewOnTwitterButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(viewInTwitterTapped(_:)))
return button
}()
private lazy var closeButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(closeTapped(_:)))
return button
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
return label
}()
// MARK: - Internal
@IBOutlet weak var notificationTitleLabel: UILabel!
@IBOutlet weak var notificationDescriptionLabel: UILabel!
@IBOutlet weak var notificationIntensityLabel: UILabel!
@IBOutlet weak var waveTimeLabel: UILabel!
@IBOutlet weak var mapView: MKMapView! {
didSet {
mapView.delegate = self
mapView.isScrollEnabled = false
mapView.isZoomEnabled = false
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
}
}
@IBOutlet private weak var shareButton: UIButton!
@IBOutlet private weak var rateAppButton: UIButton!
@IBOutlet private weak var viewOnTwitterButton: UIButton!
@IBOutlet private weak var closeButton: UIButton!
@IBOutlet private weak var descriptionLabel: UILabel!
private var impactTimestamp: Date?
private var countdownTimer: Timer?
// MARK: - View Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
setUI()
let stackView = UIStackView(arrangedSubviews: [shareButton, rateAppButton])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.spacing = .cardVerticalSpacing
containerView.addSubview(notificationTitleLabel)
containerView.addSubview(notificationIntensityLabel)
containerView.addSubview(notificationDescriptionLabel)
containerView.addSubview(mapView)
containerView.addSubview(stackView)
containerView.addSubview(viewOnTwitterButton)
containerView.addSubview(descriptionLabel)
containerView.addSubview(closeButton)
notificationTitleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: .cardPadding).isActive = true
notificationTitleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
notificationTitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
notificationIntensityLabel.topAnchor.constraint(equalTo: notificationTitleLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
notificationIntensityLabel.leadingAnchor.constraint(equalTo: notificationTitleLabel.leadingAnchor).isActive = true
notificationIntensityLabel.trailingAnchor.constraint(equalTo: notificationTitleLabel.trailingAnchor).isActive = true
notificationDescriptionLabel.topAnchor.constraint(equalTo: notificationIntensityLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
notificationDescriptionLabel.leadingAnchor.constraint(equalTo: notificationTitleLabel.leadingAnchor).isActive = true
notificationDescriptionLabel.trailingAnchor.constraint(equalTo: notificationTitleLabel.trailingAnchor).isActive = true
mapView.topAnchor.constraint(equalTo: notificationDescriptionLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
mapView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
mapView.heightAnchor.constraint(greaterThanOrEqualToConstant: 240.0).isActive = true
shareButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
rateAppButton.heightAnchor.constraint(equalTo: shareButton.heightAnchor).isActive = true
viewOnTwitterButton.heightAnchor.constraint(equalTo: shareButton.heightAnchor).isActive = true
closeButton.heightAnchor.constraint(equalTo: shareButton.heightAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: mapView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
stackView.leadingAnchor.constraint(equalTo: notificationDescriptionLabel.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: notificationDescriptionLabel.trailingAnchor).isActive = true
viewOnTwitterButton.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
viewOnTwitterButton.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
viewOnTwitterButton.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true
descriptionLabel.topAnchor.constraint(equalTo: viewOnTwitterButton.bottomAnchor, constant: .cardPadding).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: viewOnTwitterButton.leadingAnchor).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: viewOnTwitterButton.trailingAnchor).isActive = true
closeButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
closeButton.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor).isActive = true
closeButton.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
closeButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardVerticalSpacing.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
override func updateUI() {
super.updateUI()
shareButton.setLocalizedTitle(key: "main_share_app")
rateAppButton.setLocalizedTitle(key: "main_vote")
viewOnTwitterButton.setLocalizedTitle(key: "main_twitter_see")
closeButton.setLocalizedTitle(key: "official_close")
descriptionLabel.text = NSLocalizedString("map_smartphone_magnitude", comment: "")
}
// MARK: - Public
private func setUI() {
shareButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
rateAppButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
viewOnTwitterButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
closeButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
}
private func updateUI() {
@objc
func update(with notification: EQNRealtimePushNotification?) {
// clearn any other previous notifications
notificationTitleLabel.text = ""
notificationDescriptionLabel.text = ""
notificationTitleLabel.text = "Sisma rilevato a 150km (TEST)"
notificationIntensityLabel.text = "Previsto uno scuotimento forte"
notificationDescriptionLabel.text = "Distanza 150 km - 13 minuti fa"
mapView.removeAnnotations(mapView.annotations)
guard let notification = notification else { return }
containerView.backgroundColor = backgroundColor(for: notification.relativeIntensity())
backgroundColor = backgroundColor(for: notification.relativeIntensity())
notificationTitleLabel.text = notification.title
notificationIntensityLabel.text = notification.displayBody
notificationIntensityLabel.textColor = notification.relativeIntensityColor
@@ -95,7 +190,6 @@ class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMa
notificationDescriptionLabel.text = ""
+ NSLocalizedString("official_card_distance", comment: "") + " \(distanceRound) km"
+ " - " + EQNUtility.formattedString(forTimeDifference: difference)
+ "\n" + String(format: NSLocalizedString("map_number", comment: ""), "\(notification.counter)")
}
let span = MKCoordinateSpan(latitudeDelta: 10.5, longitudeDelta: 10.5)
@@ -118,37 +212,37 @@ class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMa
annotationView.title = annotation.title
return annotationView
}
// MARK: - Actions
@IBAction private func shareAppTapped(_ sender: UIButton) {
@objc private func shareAppTapped(_ sender: UIButton) {
onTapShareApp?()
}
@IBAction private func rateAppTapped(_ sender: UIButton) {
@objc private func rateAppTapped(_ sender: UIButton) {
onTapRateApp?()
}
@IBAction private func viewInTwitterTapped(_ sender: UIButton) {
@objc private func viewInTwitterTapped(_ sender: UIButton) {
onTapOpenTwitter?()
}
@IBAction private func closeTapped(_ sender: UIButton) {
@objc private func closeTapped(_ sender: UIButton) {
onTapClose?()
}
// MARK: - Helpers
// MARK: - Private
private func backgroundColor(for intensity: Double) -> UIColor {
switch intensity {
case _ where intensity < 0.004:
return UIColor(named: "Gray (card background)")!
return AppTheme.Colors.cardBackgroundGray
case _ where intensity < 0.30:
return UIColor(named: "Green (card background)")!
return AppTheme.Colors.cardBackgroundGreen
case _ where intensity < 0.70:
return UIColor(named: "Yellow (card background)")!
return AppTheme.Colors.cardBackgroundYellow
default:
return UIColor(named: "Red (card background)")!
return AppTheme.Colors.cardBackgroundRed
}
}
}
@@ -7,49 +7,83 @@
//
import UIKit
import Shogun
class AlertsSmartphoneNetworkTableViewCell: EQNBaseTableViewCell {
@objc var smartphoneNetwork: EQNReteSmartphone? {
didSet {
updateUI()
}
}
@objc
class AlertsSmartphoneNetworkTableViewCell: EQNBaseContainerTableViewCell {
@objc var onTapButton: (() -> Void)?
override var headerText: String { NSLocalizedString("main_network", comment: "") }
// MARK: - UI
private lazy var counterLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.Colors.green
label.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
label.textAlignment = .center
return label
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
return label
}()
private lazy var coverageButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(localCovergeTapped(_:)))
return button
}()
// MARK: - Internal
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var smartphoneCounterLabel: UILabel!
@IBOutlet private weak var coverageDescriptionLabel: UILabel!
@IBOutlet private weak var localCoverageButton: UIButton!
// MARK: - View Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(counterLabel)
containerView.addSubview(descriptionLabel)
containerView.addSubview(coverageButton)
counterLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
counterLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
counterLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
descriptionLabel.topAnchor.constraint(equalTo: counterLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
coverageButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
coverageButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
coverageButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
coverageButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
coverageButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("main_network", comment: "")
coverageDescriptionLabel.text = NSLocalizedString("main_monitoring_currently2", comment: "")
localCoverageButton.setLocalizedTitle(key: "main_coverage", uppercased: true, emoji: "🗺")
override func updateUI() {
super.updateUI()
coverageButton.setLocalizedTitle(key: "main_coverage", uppercased: true, emoji: "🗺")
descriptionLabel.text = NSLocalizedString("main_monitoring_currently2", comment: "")
}
private func updateUI() {
guard let smartphoneNetwork = smartphoneNetwork else { return }
smartphoneCounterLabel.text = "\(smartphoneNetwork.counterSmartphones)"
// MARK: - Public
@objc
func update(with smartphoneNetwork: EQNReteSmartphone?) {
guard let smartphoneNetwork else { return }
counterLabel.text = "\(smartphoneNetwork.counterSmartphones)"
}
// MARK: - Actions
@IBAction private func localCovergeTapped(_ sender: UIButton) {
@objc private func localCovergeTapped(_ sender: UIButton) {
onTapButton?()
}
}
@@ -98,8 +98,8 @@ class PasquakesMapViewController: EQNBaseMapViewController {
.first
// controlliamo che sia inferiore al raggio massimo impostato per le notifiche
if let radiusLow = Double(EQNAllertaSismica.shared().raggioSismiLievi),
let radiusStrong = Double(EQNAllertaSismica.shared().raggioSismiForti),
if let radiusLow = Double(EQNSettingRealTimeAlert.shared.raggioSismiLievi),
let radiusStrong = Double(EQNSettingRealTimeAlert.shared.raggioSismiForti),
let nearestPastquake = nearestPastquake {
let radius = max(radiusLow, radiusStrong)
if abs(nearestPastquake.coordinate.distance(from: userPosition)) < radius {
@@ -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()
}
}
@@ -92,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
@@ -119,7 +133,7 @@ static NSString * const SegueIdentifierLogs = @"ShowLogs";
// if user switch from settings page, we need to force a settings save
UIViewController *controller = [self getTopControllerFromController:tabBarController.selectedViewController];
if ([controller isKindOfClass:[SettingsViewController class]]) {
[SettingsBaseViewController saveSettings];
[SettingsBaseTableViewController saveSettings];
}
return YES;
@@ -1,151 +0,0 @@
//
// SubscriptionDetailViewController.swift
// Earthquake Network
//
// Created by Busi Andrea on 29/07/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
import UIKit
import SafariServices
import StoreKit
class SubscriptionDetailViewController: UIViewController {
/// Enable this allows shake to enable the current subscription
private static let ShakeToEnableSubscription = false
var product: SKProduct? {
didSet {
updateUI()
}
}
@IBOutlet private weak var containerView: UIView!
@IBOutlet private weak var productTitleLabel: UILabel!
@IBOutlet private weak var productImageView: UIImageView!
@IBOutlet private weak var productDescriptionLabel: UILabel!
@IBOutlet private weak var subscriptionDetailsLabel: UILabel!
@IBOutlet private weak var openPrivacyButton: UIButton!
@IBOutlet private weak var openTermsButton: UIButton!
@IBOutlet private weak var purchaseRecapLabel: UILabel!
@IBOutlet private weak var productPriceLabel: UILabel!
@IBOutlet private weak var purchaseButton: UIButton!
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(handlePurchaseNotification(_:)),
name: .EQNInAppPurchaseDidComplete,
object: nil)
updateUI()
setupUI()
}
// MARK: - Private
private func setupUI() {
containerView.eqn_applyShadowAndRoundedCorners()
}
private func updateUI() {
guard let product = product, isViewLoaded else { return }
productImageView.image = VersioneProProducts.image(for: product.productIdentifier)
productTitleLabel.text = product.localizedTitle
productDescriptionLabel.text = product.localizedDescription
var purchaseRecapString = ""
var subscriptionDetailsString = ""
switch product.productIdentifier {
case VersioneProProducts.Identifier.Subscription10kMonthly,
VersioneProProducts.Identifier.Subscription100kMonthly:
purchaseRecapString = "inapp_monthly_payment"
subscriptionDetailsString = "inapp_detail_description"
case VersioneProProducts.Identifier.Subscription100kYearly,
VersioneProProducts.Identifier.Subscription100kYearlyDiscounted,
VersioneProProducts.Identifier.Subscription10kYearly,
VersioneProProducts.Identifier.Subscription10kYearlyDiscounted:
purchaseRecapString = "inapp_yearly_payment"
subscriptionDetailsString = "inapp_detail_description"
case VersioneProProducts.Identifier.Subscription10kPerpetual,
VersioneProProducts.Identifier.Subscription100kPerpetual:
purchaseRecapString = "inapp_lifetime_payment"
subscriptionDetailsString = "inapp_lifetime_detail_description"
default:
break
}
subscriptionDetailsLabel.text = NSLocalizedString(subscriptionDetailsString, comment: "")
openPrivacyButton.setTitle(NSLocalizedString("network_pro_privacy_disclaimer", comment: ""), for: .normal)
openTermsButton.setTitle(NSLocalizedString("network_pro_terms_conditions", comment: ""), for: .normal)
purchaseRecapLabel.text = "\(product.localizedDescription), \(NSLocalizedString(purchaseRecapString, comment: ""))"
priceFormatter.locale = product.priceLocale
productPriceLabel.text = priceFormatter.string(from: product.price)
purchaseButton.setTitle(NSLocalizedString("inapp_purchase", comment: ""), for: .normal)
}
// MARK: - Notifications
@objc func handlePurchaseNotification(_ notification: Notification) {
navigationController?.popViewController(animated: true)
}
// MARK: - Actions
@IBAction func openExternalLinkTapped(_ sender: UIButton) {
var linkUrl: URL?
if sender == openPrivacyButton {
linkUrl = URL(string: "\(EQNWebsiteAddress)/privacy/")
} else if sender == openTermsButton {
linkUrl = URL(string: "\(EQNWebsiteAddress)/terms-conditions/")
}
if let url = linkUrl {
let controller = SFSafariViewController(url: url)
present(controller, animated: true, completion: nil)
}
}
@IBAction func subscribeTapped(_ sender: UIButton) {
guard let product = product else { return }
VersioneProProducts.store.buyProduct(product)
}
// MARK: - Helper
private var priceFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.formatterBehavior = .behavior10_4
formatter.numberStyle = .currency
return formatter
}()
}
extension SubscriptionDetailViewController {
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
guard let product = product, event?.subtype == .motionShake, Self.ShakeToEnableSubscription else {
return
}
let alert = UIAlertController(title: "🧑‍💻", message: "Please select an action", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Reset all purchases", style: .default) { action in
EQNPurchaseUtility.resetInAppPurchases()
})
alert.addAction(UIAlertAction(title: "Activate this subscription", style: .default) { action in
EQNPurchaseUtility.simulateProPurchase(identifier: product.productIdentifier)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
}
@@ -0,0 +1,204 @@
//
// SubscriptionDetailsTableViewCell.swift
// Earthquake Network
//
// Created by Andrea Busi on 18/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import UIKit
class SubscriptionDetailsTableViewCell: EQNBaseContainerTableViewCell {
var onTapPrivacy: () -> Void = { }
var onTapTerms: () -> Void = { }
var onTapPurchase: () -> Void = { }
var onChangePlan: (_ type: EQNInAppProducts.Plan) -> Void = { _ in }
override var isHeaderVisible: Bool { false }
// MARK: - UI
lazy var planSegmentedControl: UISegmentedControl = {
let control = UISegmentedControl(items: EQNInAppProducts.Plan.allCases.map(\.localizedTitle))
control.translatesAutoresizingMaskIntoConstraints = false
control.addTarget(self, action: #selector(onChangeSegmentedControl(_:)), for: .valueChanged)
return control
}()
lazy var productTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .title1)
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
lazy var productImageView: UIImageView = {
let imageView = UIImageView(image: .init(named: "top_100k"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.heightAnchor.constraint(greaterThanOrEqualToConstant: 50.0).isActive = true
return imageView
}()
lazy var subscriptionDetailsLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .justified
label.numberOfLines = 0
return label
}()
lazy var openPrivacyButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(onTapOpenPrivacyButton(_:)), for: .touchUpInside)
button.contentHorizontalAlignment = .leading
return button
}()
lazy var openTermsButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(onTapOpenTermsButton(_:)), for: .touchUpInside)
button.contentHorizontalAlignment = .leading
return button
}()
lazy var purchaseRecapLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .headline)
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
lazy var productPriceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .largeTitle)
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
lazy var purchaseButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(onTapPurchaseButton(_:)), for: .touchUpInside)
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
button.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
button.backgroundColor = .systemGroupedBackground
button.eqn_applyShadowAndRoundedCorners()
return button
}()
// MARK: - Internal
override func setupUI() {
super.setupUI()
containerView.addSubview(planSegmentedControl)
containerView.addSubview(productTitleLabel)
containerView.addSubview(productImageView)
containerView.addSubview(subscriptionDetailsLabel)
containerView.addSubview(openPrivacyButton)
containerView.addSubview(openTermsButton)
containerView.addSubview(purchaseRecapLabel)
containerView.addSubview(productPriceLabel)
containerView.addSubview(purchaseButton)
let leading: NSLayoutXAxisAnchor = planSegmentedControl.leadingAnchor
let trailing: NSLayoutXAxisAnchor = planSegmentedControl.trailingAnchor
planSegmentedControl.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
planSegmentedControl.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
planSegmentedControl.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
productTitleLabel.topAnchor.constraint(equalTo: planSegmentedControl.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
productTitleLabel.leadingAnchor.constraint(equalTo: leading, constant: .cardPadding).isActive = true
productTitleLabel.trailingAnchor.constraint(equalTo: trailing, constant: .cardPadding.negative).isActive = true
productImageView.topAnchor.constraint(equalTo: productTitleLabel.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
productImageView.leadingAnchor.constraint(equalTo: leading).isActive = true
productImageView.trailingAnchor.constraint(equalTo: trailing).isActive = true
purchaseRecapLabel.topAnchor.constraint(equalTo: productImageView.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
purchaseRecapLabel.leadingAnchor.constraint(equalTo: leading).isActive = true
purchaseRecapLabel.trailingAnchor.constraint(equalTo: trailing).isActive = true
productPriceLabel.topAnchor.constraint(equalTo: purchaseRecapLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
productPriceLabel.leadingAnchor.constraint(equalTo: leading).isActive = true
productPriceLabel.trailingAnchor.constraint(equalTo: trailing).isActive = true
purchaseButton.topAnchor.constraint(equalTo: productPriceLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
purchaseButton.leadingAnchor.constraint(equalTo: leading).isActive = true
purchaseButton.trailingAnchor.constraint(equalTo: trailing).isActive = true
subscriptionDetailsLabel.topAnchor.constraint(equalTo: purchaseButton.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
subscriptionDetailsLabel.leadingAnchor.constraint(equalTo: leading).isActive = true
subscriptionDetailsLabel.trailingAnchor.constraint(equalTo: trailing).isActive = true
openPrivacyButton.topAnchor.constraint(equalTo: subscriptionDetailsLabel.bottomAnchor, constant: .cardVerticalSpacing.x2).isActive = true
openPrivacyButton.leadingAnchor.constraint(equalTo: leading).isActive = true
openPrivacyButton.trailingAnchor.constraint(equalTo: trailing).isActive = true
openTermsButton.topAnchor.constraint(equalTo: openPrivacyButton.bottomAnchor, constant: .cardPadding).isActive = true
openTermsButton.leadingAnchor.constraint(equalTo: leading).isActive = true
openTermsButton.trailingAnchor.constraint(equalTo: trailing).isActive = true
openTermsButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardVerticalSpacing.x2.negative).isActive = true
}
override func updateUI() {
super.updateUI()
openPrivacyButton.setTitle(NSLocalizedString("network_pro_privacy_disclaimer", comment: ""), for: .normal)
openTermsButton.setTitle(NSLocalizedString("network_pro_terms_conditions", comment: ""), for: .normal)
purchaseButton.setTitle(NSLocalizedString("inapp_purchase", comment: ""), for: .normal)
}
// MARK: - Actions
@objc private func onTapOpenPrivacyButton(_ sender: UIButton) {
onTapPrivacy()
}
@objc private func onTapOpenTermsButton(_ sender: UIButton) {
onTapTerms()
}
@objc private func onTapPurchaseButton(_ sender: UIButton) {
onTapPurchase()
}
@objc private func onChangeSegmentedControl(_ sender: UISegmentedControl) {
let type: EQNInAppProducts.Plan = .from(index: sender.selectedSegmentIndex)
onChangePlan(type)
}
}
extension EQNInAppProducts.Plan {
var index: Int {
switch self {
case .monthly: 0
case .yearly: 1
case .perpetual: 2
}
}
static func from(index: Int) -> Self {
switch index {
case 0: .monthly
case 1: .yearly
default: .perpetual
}
}
}
@@ -0,0 +1,183 @@
//
// SubscriptionDetailsViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 18/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import UIKit
import StoreKit
import SafariServices
import Shogun
class SubscriptionDetailsViewController: UITableViewController {
/// Enable this allows shake to enable the current subscription
private static let ShakeToEnableSubscription = false
// MARK: - Internal
private let products: [EQNInAppProducts]
private var selectedProduct: EQNInAppProducts {
didSet {
onProductSelected()
}
}
private var priceFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.formatterBehavior = .behavior10_4
formatter.numberStyle = .currency
return formatter
}()
// MARK: - Init
init(
products: [EQNInAppProducts]
) {
self.products = products
self.selectedProduct = products.first(where: { $0.plan == .yearly }) ?? products.first!
super.init(style: .plain)
}
required init?(coder: NSCoder) {
fatalError("Please use init(products:) instead.")
}
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
addObservers()
}
private func addObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(handlePurchaseNotification(_:)),
name: .EQNInAppPurchaseDidComplete,
object: nil)
}
private func configureUI() {
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 2000.0
tableView.separatorStyle = .none
tableView.backgroundColor = .systemGroupedBackground
tableView.contentInset = EQNBaseContainerTableViewCell.EdgeInsets
tableView.registerCell(for: SubscriptionDetailsTableViewCell.self)
}
// MARK: - Notifications
@objc private func handlePurchaseNotification(_ notification: Notification) {
navigationController?.popViewController(animated: true)
}
// MARK: - Table view delegate & data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(cellIdentifiable: SubscriptionDetailsTableViewCell.self, for: indexPath)
cell.selectionStyle = .none
cell.productTitleLabel.text = selectedProduct.product.localizedTitle
cell.productImageView.image = selectedProduct.category.image
var purchaseRecapString = ""
var subscriptionDetailsString = ""
switch selectedProduct.productIdentifier {
case EQNInAppProducts.Identifier.Subscription10kMonthly,
EQNInAppProducts.Identifier.Subscription100kMonthly:
purchaseRecapString = "inapp_monthly_payment"
subscriptionDetailsString = "inapp_detail_description"
case EQNInAppProducts.Identifier.Subscription100kYearly,
EQNInAppProducts.Identifier.Subscription100kYearlyDiscounted,
EQNInAppProducts.Identifier.Subscription10kYearly,
EQNInAppProducts.Identifier.Subscription10kYearlyDiscounted:
purchaseRecapString = "inapp_yearly_payment"
subscriptionDetailsString = "inapp_detail_description"
case EQNInAppProducts.Identifier.Subscription10kPerpetual,
EQNInAppProducts.Identifier.Subscription100kPerpetual:
purchaseRecapString = "inapp_lifetime_payment"
subscriptionDetailsString = "inapp_lifetime_detail_description"
default:
break
}
cell.subscriptionDetailsLabel.text = NSLocalizedString(subscriptionDetailsString, comment: "")
cell.onTapPrivacy = { [weak self] in
self?.openExternalLink("\(EQNWebsiteAddress)/privacy/")
}
cell.onTapTerms = { [weak self] in
self?.openExternalLink("\(EQNWebsiteAddress)/terms-conditions/")
}
cell.onTapPurchase = { [weak self] in
self?.purchaseSelectedProduct()
}
cell.onChangePlan = { [weak self] type in
if let product = self?.productFromProductType(type) {
self?.selectedProduct = product
}
}
cell.planSegmentedControl.selectedSegmentIndex = selectedProduct.plan.index
cell.purchaseRecapLabel.text = "\(selectedProduct.product.localizedDescription), \(NSLocalizedString(purchaseRecapString, comment: ""))"
cell.productPriceLabel.text = priceFormatter.string(from: selectedProduct.product.price)
return cell
}
// MARK: - Private
private func onProductSelected() {
priceFormatter.locale = selectedProduct.product.priceLocale
tableView.reloadData()
}
private func openExternalLink(_ stringUrl: String) {
if let url = URL(string: stringUrl) {
let controller = SFSafariViewController(url: url)
present(controller, animated: true, completion: nil)
}
}
private func purchaseSelectedProduct() {
EQNInAppProducts.store.buyProduct(selectedProduct.product)
}
private func productFromProductType(_ type: EQNInAppProducts.Plan) -> EQNInAppProducts? {
let product: EQNInAppProducts?
switch type {
case .monthly:
product = products.first { $0.plan == .monthly }
case .yearly:
product = products.first { $0.plan == .yearly }
case .perpetual:
product = products.first { $0.plan == .perpetual }
}
return product
}
}
extension SubscriptionDetailsViewController {
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
guard event?.subtype == .motionShake, Self.ShakeToEnableSubscription else {
return
}
let alert = UIAlertController(title: "🧑‍💻", message: "Please select an action", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Reset all purchases", style: .default) { action in
EQNPurchaseUtility.resetInAppPurchases()
})
alert.addAction(UIAlertAction(title: "Activate this subscription", style: .default) { action in
EQNPurchaseUtility.simulateProPurchase(identifier: self.selectedProduct.productIdentifier)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
}
@@ -9,57 +9,81 @@
import UIKit
import StoreKit
class SubscriptionProductTableViewCell: UITableViewCell {
class SubscriptionProductTableViewCell: EQNBaseContainerTableViewCell {
override var isHeaderVisible: Bool { false }
override var isRightArrowVisbile: Bool { true }
// MARK: - UI
private lazy var productImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
return imageView
}()
var product: SKProduct? {
didSet {
updateUI()
}
}
var availability: EQNPurchaseAvailability? {
didSet {
updateUI()
}
}
private lazy var productTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .headline)
label.numberOfLines = 0
return label
}()
@IBOutlet private weak var productImageView: UIImageView!
@IBOutlet private weak var productTitleLabel: UILabel!
@IBOutlet private weak var productDescriptionLabel: UILabel?
@IBOutlet private weak var productInfoLabel: UILabel!
// MARK: - View Lifecycle
// force an inset to have the same style of EQNBaseTableViewCell
override var frame: CGRect {
get {
return super.frame
}
set (newFrame) {
let inset: CGFloat = 8
var frame = newFrame
frame.origin.x += inset
frame.size.width -= 2 * inset
super.frame = frame
}
}
// MARK: - Private
private func updateUI() {
guard let product = product else { return }
private lazy var productInfoLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.Colors.red
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 0
return label
}()
productImageView.image = VersioneProProducts.image(for: product.productIdentifier)
productTitleLabel.text = product.localizedTitle
productDescriptionLabel?.text = product.localizedDescription
// MARK: - Internal
override func setupUI() {
super.setupUI()
let infoKey = VersioneProProducts.is100kSubscription(for: product.productIdentifier) ? "inapp_available_100k" : "inapp_available_10k"
let counter = availability(for: product.productIdentifier)
containerView.addSubview(productImageView)
containerView.addSubview(productTitleLabel)
containerView.addSubview(productInfoLabel)
productImageView.widthAnchor.constraint(equalToConstant: 80.0).isActive = true
productImageView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
productTitleLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
productTitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
productImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
productImageView.trailingAnchor.constraint(equalTo: productTitleLabel.leadingAnchor, constant: .cardPadding.negative).isActive = true
productImageView.centerYAnchor.constraint(equalTo: productTitleLabel.centerYAnchor).isActive = true
productInfoLabel.topAnchor.constraint(equalTo: productTitleLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
productInfoLabel.leadingAnchor.constraint(equalTo: productImageView.leadingAnchor).isActive = true
productInfoLabel.trailingAnchor.constraint(equalTo: productTitleLabel.trailingAnchor).isActive = true
productInfoLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardVerticalSpacing.negative).isActive = true
}
// MARK: - Public
func update(
category: EQNInAppProducts.Category,
availability: EQNPurchaseAvailability?
) {
productImageView.image = category.image
productTitleLabel.text = category.localizedTitle
let infoKey = category == .top100k ? "inapp_available_100k" : "inapp_available_10k"
let counter = availabilityCounter(for: category, availability: availability)
productInfoLabel.text = String(format: NSLocalizedString(infoKey, comment: ""), counter)
}
private func availability(for productIdentifier: String) -> Int {
if VersioneProProducts.is100kSubscription(for: productIdentifier) {
private func availabilityCounter(
for category: EQNInAppProducts.Category,
availability: EQNPurchaseAvailability?
) -> Int {
if category == .top100k {
return availability?.top100kAvailable ?? 0
}
return availability?.top10kAvailable ?? 0
@@ -9,38 +9,87 @@
import UIKit
import StoreKit
class SubscriptionsActiveTableViewCell: EQNBaseTableViewCell {
var product: SKProduct? {
didSet {
updateUI()
}
}
class SubscriptionsActiveTableViewCell: EQNBaseContainerTableViewCell {
override var headerText: String { NSLocalizedString("inapp_active", comment: "") }
var onTapRestore: () -> Void = { }
// MARK: - UI
private lazy var noSubscriptionsLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var noSubscriptionsLabel: UILabel!
@IBOutlet private weak var activeSubscriptionImageView: UIImageView!
private lazy var activeSubscriptionImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
return imageView
}()
// MARK: - View Lifecycle
private lazy var restoreButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(restoreSubscriptionsTapped(_:)), for: .touchUpInside)
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
button.backgroundColor = .systemGroupedBackground
button.eqn_applyShadowAndRoundedCorners()
return button
}()
// MARK: - Internal
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
let stackView = UIStackView(arrangedSubviews: [ activeSubscriptionImageView, noSubscriptionsLabel, restoreButton ])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.alignment = .center
stackView.distribution = .equalSpacing
stackView.axis = .vertical
stackView.spacing = 20.0
containerView.addSubview(stackView)
activeSubscriptionImageView.widthAnchor.constraint(equalToConstant: 150.0).isActive = true
activeSubscriptionImageView.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
restoreButton.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
restoreButton.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
restoreButton.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
stackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardVerticalSpacing.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("inapp_active", comment: "")
override func updateUI() {
super.updateUI()
noSubscriptionsLabel.text = NSLocalizedString("inapp_nosub", comment: "")
restoreButton.setTitle(NSLocalizedString("purchase_pro_restore", comment: ""), for: .normal)
}
private func updateUI() {
if let productIdentifier = product?.productIdentifier {
// MARK: - Actions
@objc private func restoreSubscriptionsTapped(_ sender: UIButton) {
onTapRestore()
}
// MARK: - Public
func update(with product: EQNInAppProducts?) {
if let product {
noSubscriptionsLabel.isHidden = true
activeSubscriptionImageView.isHidden = false
activeSubscriptionImageView.image = VersioneProProducts.image(for: productIdentifier)
activeSubscriptionImageView.image = product.category.image
} else {
noSubscriptionsLabel.isHidden = false
activeSubscriptionImageView.isHidden = true
@@ -8,21 +8,40 @@
import UIKit
class SubscriptionsDescriptionTableViewCell: EQNBaseTableViewCell {
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var descriptionLabel: UILabel!
class SubscriptionsDescriptionTableViewCell: EQNBaseContainerTableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
override var headerText: String { NSLocalizedString("inapp_list", comment: "") }
// MARK: - UI
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 0
label.textAlignment = .justified
return label
}()
// MARK: - Internal
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(descriptionLabel)
descriptionLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
descriptionLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardVerticalSpacing.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("inapp_list", comment: "")
override func updateUI() {
super.updateUI()
descriptionLabel.text = NSLocalizedString("inapp_description", comment: "")
}
}
@@ -8,26 +8,55 @@
import UIKit
class SubscriptionsHeaderTableViewCell: UITableViewCell {
var isLoading = false {
didSet {
updateUI()
}
class SubscriptionsHeaderTableViewCell: UITableViewHeaderFooterView {
// MARK: - UI
private lazy var headerTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.preferredFont(forTextStyle: .title2)
label.textColor = AppTheme.Colors.darkGray
label.textAlignment = .center
return label
}()
private lazy var loadingActivityIndicator: UIActivityIndicatorView = {
let spinner = UIActivityIndicatorView(style: .medium)
spinner.translatesAutoresizingMaskIntoConstraints = false
spinner.hidesWhenStopped = true
return spinner
}()
// MARK: - Init
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
setupUI()
}
var title: String? = nil {
didSet {
updateUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
@IBOutlet private weak var headerTitleLabel: UILabel!
@IBOutlet private weak var loadingActivityIndicator: UIActivityIndicatorView!
// MARK: - Private
private func updateUI() {
private func setupUI() {
contentView.addSubview(headerTitleLabel)
contentView.addSubview(loadingActivityIndicator)
headerTitleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
headerTitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: .cardPadding).isActive = true
headerTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: .cardPadding.negative).isActive = true
loadingActivityIndicator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: .cardPadding.negative).isActive = true
loadingActivityIndicator.centerYAnchor.constraint(equalTo: headerTitleLabel.centerYAnchor).isActive = true
}
// MARK: - Public
func update(isLoading: Bool, title: String?) {
headerTitleLabel.text = title
if isLoading && title != nil {
@@ -10,38 +10,29 @@ import UIKit
import StoreKit
import Shogun
@objc
class SubscriptionsViewController: UITableViewController {
private static let SegueIdentifierSubscriptionDetail = "ShowSubscriptionDetail"
private static let CellHeightDescription: CGFloat = 320.0
// sezioni
private enum TableSection: CaseIterable {
case active
case description
case monthly
case yearly
case perpetual
case products
var sectionTitle: String? {
switch self {
case .monthly: return NSLocalizedString("inapp_monthly_subscriptions", comment: "")
case .yearly: return NSLocalizedString("inapp_yearly_subscriptions", comment: "")
case .perpetual: return NSLocalizedString("inapp_lifetime_subscriptions", comment: "")
default: return nil
case .products: NSLocalizedString("subscriptions_available", comment: "")
default: nil
}
}
}
private let sections = TableSection.allCases
private var allProducts = [SKProduct]()
private var monthlyProducts = [SKProduct]()
private var yearlyProducts = [SKProduct]()
private var perpetualProducts = [SKProduct]()
/// All products retrieved from AppStore
private var products = [EQNInAppProducts]()
/// Product already bought by the user
private var subscribedProduct: SKProduct?
private var productSubscribed: EQNInAppProducts?
/// Availability for subscriptions
private var availability: EQNPurchaseAvailability?
/// Tells if products are loading
@@ -49,6 +40,13 @@ class SubscriptionsViewController: UITableViewController {
/// Tells if a restore is in progress
private var isRestorePurchase = false
// MARK: - Init
@objc
convenience init() {
self.init(style: .plain)
}
// MARK: - View Lifecycle
override func viewDidLoad() {
@@ -64,15 +62,7 @@ class SubscriptionsViewController: UITableViewController {
loadData()
checkAvailabilities()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == Self.SegueIdentifierSubscriptionDetail,
let controller = segue.destination as? SubscriptionDetailViewController,
let product = sender as? SKProduct {
controller.product = product
}
}
// MARK: - Private
private func addObservers() {
@@ -90,75 +80,62 @@ 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.reloadData()
tableView.estimatedRowHeight = 600.0
tableView.separatorStyle = .none
tableView.backgroundColor = .systemGroupedBackground
tableView.contentInset = EQNBaseContainerTableViewCell.EdgeInsets
// remove extra padding on top of each section header
tableView.sectionHeaderTopPadding = 0.0
tableView.registerCell(for: SubscriptionsActiveTableViewCell.self)
tableView.registerCell(for: SubscriptionsDescriptionTableViewCell.self)
tableView.registerCell(for: SubscriptionProductTableViewCell.self)
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.isTop10k
let rIs10k = rProduct.isTop10k
// If left item is Top10k, order first
if lIs10k && !rIs10k {
return true
} else if !lProduct.isTop10k && rProduct.isTop10k {
// right product is Top10k, left no
return false
} else {
// both products Top10k or Top100k, keep existing order
return false
}
}
self.productSubscribed = purchased.first
self.products = products.sorted(by: { $0.productIdentifier > $1.productIdentifier })
self.tableView.reloadData()
}
}
@@ -171,18 +148,13 @@ class SubscriptionsViewController: UITableViewController {
EQNPurchaseUtility.availableSubscriptions { (availability) in
DispatchQueue.main.async {
self.availability = availability
self.updateUI()
self.tableView.reloadData()
}
}
}
// MARK: - Actions
@objc func restoreTapped(_ sender: AnyObject) {
isRestorePurchase = true
VersioneProProducts.store.restorePurchases()
}
@objc func closeTapped(_ sender: AnyObject) {
dismiss(animated: true, completion: nil)
}
@@ -190,7 +162,7 @@ class SubscriptionsViewController: UITableViewController {
// MARK: - Notifications
@objc func fail(_ notification: Notification){
VersioneProProducts.store.loadPurchase()
EQNInAppProducts.store.loadPurchase()
}
@objc func handlePurchaseNotification(_ notification: Notification) {
@@ -209,7 +181,7 @@ class SubscriptionsViewController: UITableViewController {
present(alert, animated: true, completion: nil)
}
VersioneProProducts.store.loadPurchase()
EQNInAppProducts.store.loadPurchase()
loadData()
}
@@ -228,20 +200,22 @@ class SubscriptionsViewController: UITableViewController {
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let tableSection = sections[section]
if let cell = tableView.dequeueReusableCell(withIdentifier: "SectionHeaderCell") as? SubscriptionsHeaderTableViewCell {
cell.title = tableSection.sectionTitle
cell.isLoading = isLoading
return cell
switch tableSection.sectionTitle {
case .some(let title):
let view = tableView.dequeueHeaderFooterView(cellIdentifiable: SubscriptionsHeaderTableViewCell.self)
view.update(isLoading: isLoading, title: title)
return view
case .none:
return nil
}
return nil
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let tableSection = sections[section]
if tableSection.sectionTitle != nil {
return 50
return switch tableSection.sectionTitle {
case .some: 50.0
case .none: 0.0
}
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -249,96 +223,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,187 @@
//
// MapSeismicWaveAnimator.swift
// Earthquake Network
//
// Created by Andrea Busi on 24/07/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import UIKit
import MapKit
class MapSeismicWaveAnimator {
private weak var mapView: MKMapView?
private weak var waveTimeLabel: UILabel?
private let OverlayCircleId = "wave_animation"
/// Alert to display
private let realtimeAlert: EQNRealtimePushNotification
/// Timer to constantly update countdown label
private var countdownTimer: Timer?
/// Timer to simulate animation for the wave
private var waveAnimationTimer: Timer?
/// Refresh time for wave animation
private let waveAnimationRefreshRate = 0.1
/// Current radius of the wave animation on the map
private var waveAnimationCurrentRadius: CLLocationDistance = 0
private var waveAnimationVelocity: Double = 1_000
// MARK: - Init
init(
realtimeAlert: EQNRealtimePushNotification,
mapView: MKMapView,
waveTimeLabel: UILabel
) {
self.realtimeAlert = realtimeAlert
self.mapView = mapView
self.waveTimeLabel = waveTimeLabel
self.setup()
}
private func setup() {
self.waveAnimationCurrentRadius = currentWavePosition()
self.waveAnimationVelocity = evaluateWaveAnimationVelocity()
}
// MARK: - Public
func start() {
startCountdown()
startWaveAnimation()
}
func stop() {
stopCountdown()
stopWaveAnimation()
}
// MARK: - Wave
private func startCountdown() {
// show countdown only if time is less than 300 seconds
if realtimeAlert.currentCountdown() < 300 {
// start a timer for the countdown label
waveTimeLabel?.isHidden = false
countdownTimer = Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(countdownTimerFired(_:)),
userInfo: nil,
repeats: true
)
countdownTimer?.fire()
}
}
private func startWaveAnimation() {
waveAnimationTimer = Timer.scheduledTimer(
timeInterval: waveAnimationRefreshRate,
target: self,
selector: #selector(mapWaveAnimationFired(_:)),
userInfo: nil,
repeats: true
)
waveAnimationTimer?.fire()
}
private func stopCountdown() {
countdownTimer?.invalidate()
countdownTimer = nil
}
private func stopWaveAnimation() {
waveAnimationTimer?.invalidate()
waveAnimationTimer = nil
}
// MARK: - Timer
@objc private func countdownTimerFired(_ sender: Timer) {
let countdown = realtimeAlert.currentCountdown()
waveTimeLabel?.text = String.localizedStringWithFormat(NSLocalizedString("alert_wave", comment: ""), countdown)
waveTimeLabel?.textColor = waveTimeTextColor(for: countdown)
if countdown <= 0 {
// stop the countdown
stopCountdown()
}
}
@objc private func mapWaveAnimationFired(_ sender: Timer) {
waveAnimationCurrentRadius += waveAnimationVelocity
addMapCircle(center: realtimeAlert.coordinate.coordinate, radius: waveAnimationCurrentRadius)
}
// MARK: - Helpers
/// Evaluate current position for the wave
/// Used to define initial position for the wave circle
/// - Returns: Distance of the wave from the original earthquake point
private func currentWavePosition() -> Double {
// distanza tra utente e terremoto
let distance = realtimeAlert.distanceFromUser()
// calcoliamo la distanza rimanente da mostrare, perchè la schermata potrebbe anche essere aperta in ritardo
let remainingDistance = realtimeAlert.waveSpeed * Double(realtimeAlert.currentCountdown())
return distance - remainingDistance
}
/// Evaluate wave velocity based on push notification data
/// - Returns: Wave velocity, used for animation
private func evaluateWaveAnimationVelocity() -> Double {
let velocity = realtimeAlert.waveSpeed
return velocity * waveAnimationRefreshRate
}
/// Returns the text color based on impact countdown
private func waveTimeTextColor(for countdown: Int) -> UIColor {
switch countdown {
case _ where countdown > 15:
return UIColor(red: 255.0/255.0, green: 140.0/255.0, blue: 0.0, alpha: 1.0)
case _ where countdown > 5:
return UIColor(red: 255.0/255.0, green: 100.0/255.0, blue: 0.0, alpha: 1.0)
default:
return UIColor(red: 255.0/255.0, green: 0.0/255.0, blue: 0.0, alpha: 1.0)
}
}
// MARK: - Map management
func addMapCircle(
center: CLLocationCoordinate2D,
radius: CLLocationDistance
) {
guard let mapView else { return }
// remove any other existing overlays
let overlays = mapView.overlays.filter { $0.title == OverlayCircleId }
mapView.removeOverlays(overlays)
// add new overlay
let circle = MKCircle(center: center, radius: radius)
circle.title = OverlayCircleId
mapView.addOverlay(circle)
}
func getOverlayRenderer(for overlay: MKOverlay) -> MKOverlayRenderer? {
switch overlay {
case let circle as MKCircle where overlay.title == OverlayCircleId:
let circleRenderer = MKCircleRenderer(overlay: circle)
circleRenderer.strokeColor = AppTheme.Colors.red
circleRenderer.fillColor = AppTheme.Colors.red.withAlphaComponent(0.2)
circleRenderer.lineWidth = 3.0
return circleRenderer
case let polyline as MKPolyline:
let polylineRenderer = MKPolylineRenderer(polyline: polyline)
polylineRenderer.strokeColor = .blue
polylineRenderer.lineWidth = 2.0
return polylineRenderer
default:
return nil
}
}
}
@@ -15,6 +15,8 @@ class RealtimeAlertContainerView: UIView {
lazy var alertView: RealtimeAlertView = {
let view = RealtimeAlertView()
view.translatesAutoresizingMaskIntoConstraints = false
view.eqn_applyRoundedCorners()
view.clipsToBounds = true
return view
}()
@@ -78,10 +80,11 @@ class RealtimeAlertView: UIView {
let waveTimeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .title2)
label.font = .preferredFont(forTextStyle: .largeTitle)
label.textColor = AppTheme.Colors.red
label.text = String.localizedStringWithFormat(NSLocalizedString("alert_wave", comment: ""), 0)
label.textAlignment = .center
label.numberOfLines = 2
label.isHidden = true
return label
}()
@@ -89,7 +92,7 @@ class RealtimeAlertView: UIView {
let intensityLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .title3)
label.font = .preferredFont(forTextStyle: .largeTitle)
label.textColor = AppTheme.Colors.red
label.textAlignment = .center
label.numberOfLines = 2
@@ -99,7 +102,6 @@ class RealtimeAlertView: UIView {
lazy var mapView: MKMapView = {
let map = MKMapView()
map.translatesAutoresizingMaskIntoConstraints = false
map.delegate = self
map.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
map.showsUserLocation = true
return map
@@ -157,28 +159,13 @@ class RealtimeAlertView: UIView {
}
// MARK: - Public
func addMapCircle(
center: CLLocationCoordinate2D,
radius: CLLocationDistance,
overlayId: String
func addMapLine(
coordinates: [CLLocationCoordinate2D]
) {
// remove any other existing overlays
let overlays = mapView.overlays.filter { $0.title == overlayId }
mapView.removeOverlays(overlays)
// add new overlay
let circle = MKCircle(center: center, radius: radius)
circle.title = overlayId
mapView.addOverlay(circle)
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
mapView.addOverlay(polyline)
}
func addMapLine(
coordinates: [CLLocationCoordinate2D]
) {
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
mapView.addOverlay(polyline)
}
func addMapAnnotation(
title: String = "",
@@ -189,35 +176,3 @@ class RealtimeAlertView: UIView {
mapView.addAnnotation(annotation)
}
}
extension RealtimeAlertView: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
switch overlay {
case let circle as MKCircle:
let circleRenderer = MKCircleRenderer(overlay: circle)
circleRenderer.strokeColor = AppTheme.Colors.red
circleRenderer.fillColor = AppTheme.Colors.red.withAlphaComponent(0.2)
circleRenderer.lineWidth = 3.0
return circleRenderer
case let polyline as MKPolyline:
let polylineRenderer = MKPolylineRenderer(polyline: polyline)
polylineRenderer.strokeColor = .blue
polylineRenderer.lineWidth = 2.0
return polylineRenderer
default:
return MKOverlayRenderer(overlay: overlay)
}
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? EQNMapAnnotationPastquake else {
return nil
}
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: EQNCustomAnnotationView.SingleLineIdentifier, for: annotation) as! EQNCustomAnnotationView
annotationView.image = annotation.image
annotationView.title = annotation.title
return annotationView
}
}
@@ -10,7 +10,7 @@ import UIKit
import MapKit
class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
class RealtimeAlertViewController: UIViewController {
@objc var onClose: () -> Void = {}
@@ -20,17 +20,17 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
private var notificationView: RealtimeAlertView {
containerView.alertView
}
/// Manage the wave animation on the map and the countdown label
private lazy var animator: MapSeismicWaveAnimator = {
let animator = MapSeismicWaveAnimator(
realtimeAlert: realtimeAlert,
mapView: notificationView.mapView,
waveTimeLabel: notificationView.waveTimeLabel
)
return animator
}()
/// Alert to display
private let realtimeAlert: EQNRealtimePushNotification
/// Timer to constantly update countdown label
private var countdownTimer: Timer?
/// Refresh time for wave animation
private let waveAnimationRefreshRate = 0.1
/// Current radius of the wave animation on the map
private var waveAnimationCurrentRadius: CLLocationDistance = 0
private var waveAnimationVelocity: Double = 1_000
/// Timer to simulate animation for the wave
private var waveAnimationTimer: Timer?
// MARK: - Init
@@ -38,9 +38,6 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
init(notification: EQNRealtimePushNotification) {
self.realtimeAlert = notification
super.init(nibName: nil, bundle: nil)
self.waveAnimationCurrentRadius = currentWavePosition()
self.waveAnimationVelocity = evaluateWaveAnimationVelocity()
}
required init?(coder: NSCoder) {
@@ -64,8 +61,7 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
configureUI()
updateUI()
startCountdown()
startWaveAnimation()
animator.start()
}
override func viewWillAppear(_ animated: Bool) {
@@ -77,6 +73,8 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
// MARK: - Private
private func configureUI() {
notificationView.mapView.delegate = self
notificationView.closeButton.addTarget(self, action: #selector(onTapClose(_:)), for: .touchUpInside)
// configure color for animation
@@ -104,93 +102,37 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
// aggiungiamo annotation con epicentro sisma
notificationView.addMapAnnotation(center: realtimeAlert.coordinate.coordinate, intensity: realtimeAlert.intensity)
// simuliamo animazione dell'onda sismica
notificationView.addMapCircle(center: realtimeAlert.coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation")
// aggiungiamo un segmento tra la posizione del sisma e quella dell'utente
if let lastPosition = EQNUser.default().lastPosition {
notificationView.addMapLine(coordinates: [realtimeAlert.coordinate.coordinate, lastPosition.coordinate])
}
}
private func startCountdown() {
// show countdown only if time is less than 300 seconds
if realtimeAlert.currentCountdown() < 300 {
// start a timer for the countdown label
notificationView.waveTimeLabel.isHidden = false
countdownTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(countdownTimerFired(_:)), userInfo: nil, repeats: true)
countdownTimer?.fire()
}
}
private func startWaveAnimation() {
waveAnimationTimer = Timer.scheduledTimer(timeInterval: waveAnimationRefreshRate, target: self, selector: #selector(mapWaveAnimationFired(_:)), userInfo: nil, repeats: true)
waveAnimationTimer?.fire()
}
// MARK: - Action
@objc private func onTapClose(_ sender: UIButton) {
// invalidiamo i timer, altri
countdownTimer?.invalidate()
countdownTimer = nil
waveAnimationTimer?.invalidate()
waveAnimationTimer = nil
// stoppiamo animazione e countdown
animator.stop()
onClose()
dismiss(animated: true)
}
}
extension RealtimeAlertViewController: MKMapViewDelegate {
// MARK: - Timer
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
animator.getOverlayRenderer(for: overlay) ?? MKOverlayRenderer(overlay: overlay)
}
@objc private func countdownTimerFired(_ sender: Timer) {
let countdown = realtimeAlert.currentCountdown()
notificationView.waveTimeLabel.text = String.localizedStringWithFormat(NSLocalizedString("alert_wave", comment: ""), countdown)
notificationView.waveTimeLabel.textColor = waveTimeTextColor(for: countdown)
if countdown <= 0 {
// stop the countdown
countdownTimer?.invalidate()
countdownTimer = nil
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? EQNMapAnnotationPastquake else {
return nil
}
}
@objc private func mapWaveAnimationFired(_ sender: Timer) {
waveAnimationCurrentRadius += waveAnimationVelocity
notificationView.addMapCircle(center: realtimeAlert.coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation")
}
// MARK: - Helpers
/// Evaluate current position for the wave
/// Used to define initial position for the wave circle
/// - Returns: Distance of the wave from the original earthquake point
private func currentWavePosition() -> Double {
// distanza tra utente e terremoto
let distance = realtimeAlert.distanceFromUser()
// calcoliamo la distanza rimanente da mostrare, perchè la schermata potrebbe anche essere aperta in ritardo
let remainingDistance = realtimeAlert.waveSpeed * Double(realtimeAlert.currentCountdown())
return distance - remainingDistance
}
/// Evaluate wave velocity based on push notification data
/// - Returns: Wave velocity, used for animation
private func evaluateWaveAnimationVelocity() -> Double {
let velocity = realtimeAlert.waveSpeed
return velocity * waveAnimationRefreshRate
}
/// Returns the text color based on impact countdown
private func waveTimeTextColor(for countdown: Int) -> UIColor {
switch countdown {
case _ where countdown > 15:
return UIColor(red: 255.0/255.0, green: 140.0/255.0, blue: 0.0, alpha: 1.0)
case _ where countdown > 5:
return UIColor(red: 255.0/255.0, green: 100.0/255.0, blue: 0.0, alpha: 1.0)
default:
return UIColor(red: 255.0/255.0, green: 0.0/255.0, blue: 0.0, alpha: 1.0)
}
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: EQNCustomAnnotationView.SingleLineIdentifier, for: annotation) as! EQNCustomAnnotationView
annotationView.image = annotation.image
annotationView.title = annotation.title
return annotationView
}
}
@@ -9,46 +9,97 @@
import UIKit
import Shogun
class SegnalazioniLast24HoursCell: EQNBaseTableViewCell {
@objc
class SegnalazioniLast24HoursCell: EQNBaseContainerTableViewCell {
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var reportsLabel: UILabel!
@IBOutlet private weak var reportsDescriptionLabel: UILabel!
@objc var onTapTwitter: (() -> Void)?
@objc var onTapMap: (() -> Void)?
@objc var onTapTelegram: (() -> Void)?
override var isHeaderVisible: Bool { false }
// MARK: - UI
private lazy var reportsLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.Colors.red
label.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
private lazy var reportsDescriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = AppTheme.shared.cardTextColor
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
@IBOutlet private weak var twitterButton: UIButton! {
didSet {
twitterButton.imageView?.contentMode = .scaleAspectFit
}
}
@IBOutlet private weak var telegramButton: UIButton! {
didSet {
telegramButton.imageView?.contentMode = .scaleAspectFit
}
}
@IBOutlet private weak var mapButton: UIButton!
private lazy var twitterButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(twitterButtonTapped(_:)))
button.imageView?.contentMode = .scaleAspectFit
button.setImage(.init(named: "xcorp_icon"), for: .normal)
return button
}()
private lazy var mapButton: UIButton = {
let button = EQNRoundedButton.make(title: NSLocalizedString("official_button_map", comment: ""), target: self, action: #selector(mapButtonTapped(_:)))
return button
}()
private lazy var telegramButton: UIButton = {
let button = EQNRoundedButton.make(target: self, action: #selector(telegramButtonTapped(_:)))
button.imageView?.contentMode = .scaleAspectFit
button.setImage(.init(named: "telegram_icon"), for: .normal)
return button
}()
// MARK: - View Lifecycle
// MARK: - Internal
override func awakeFromNib() {
super.awakeFromNib()
override func setupUI() {
super.setupUI()
localizeUI()
containerView.addSubview(reportsLabel)
containerView.addSubview(reportsDescriptionLabel)
let stackView = UIStackView(arrangedSubviews: [twitterButton, mapButton, telegramButton])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.spacing = .cardVerticalSpacing
stackView.distribution = .fillEqually
containerView.addSubview(stackView)
reportsLabel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: .cardPadding).isActive = true
reportsLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: .cardPadding).isActive = true
reportsLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: .cardPadding.negative).isActive = true
reportsDescriptionLabel.topAnchor.constraint(equalTo: reportsLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
reportsDescriptionLabel.leadingAnchor.constraint(equalTo: reportsLabel.leadingAnchor).isActive = true
reportsDescriptionLabel.trailingAnchor.constraint(equalTo: reportsLabel.trailingAnchor).isActive = true
stackView.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0).isActive = true
stackView.topAnchor.constraint(equalTo: reportsDescriptionLabel.bottomAnchor, constant: .cardVerticalSpacing).isActive = true
stackView.leadingAnchor.constraint(equalTo: reportsDescriptionLabel.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: reportsDescriptionLabel.trailingAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: .cardPadding.negative).isActive = true
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("tab_manual", comment: "").capitalized
override func updateUI() {
super.updateUI()
reportsDescriptionLabel.text = NSLocalizedString("main_map", comment: "")
mapButton.setTitle(NSLocalizedString("official_button_map", comment: ""), for: .normal)
}
// MARK: - Public
@objc func updateUI(for smartphoneNetwork: EQNReteSmartphone?) {
guard let smartphoneNetwork = smartphoneNetwork else {
return
}
@objc
func update(with smartphoneNetwork: EQNReteSmartphone?) {
guard let smartphoneNetwork = smartphoneNetwork else { return }
let reports = smartphoneNetwork.manual
self.reportsLabel.text = "\(reports)"
@@ -61,4 +112,18 @@ class SegnalazioniLast24HoursCell: EQNBaseTableViewCell {
self.reportsLabel.textColor = UIColor(hex6: 0xff0000)
}
}
// MARK: - Actions
@objc private func twitterButtonTapped(_ sender: UIButton) {
onTapTwitter?()
}
@objc private func mapButtonTapped(_ sender: UIButton) {
onTapMap?()
}
@objc private func telegramButtonTapped(_ sender: UIButton) {
onTapTelegram?()
}
}
@@ -28,35 +28,7 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
private var filteredReports = [EQNSegnalazione]()
// MARK: - UI
// app icon and name displayed on the screenshot
private lazy var watermarkView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = true
let logo = UIImageView(image: .init(named: "eq_icon_transparent"))
logo.translatesAutoresizingMaskIntoConstraints = false
logo.contentMode = .scaleAspectFit
view.addSubview(logo)
logo.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
logo.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
logo.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
logo.widthAnchor.constraint(equalTo: logo.heightAnchor).isActive = true
let title = UILabel()
title.translatesAutoresizingMaskIntoConstraints = false
title.text = NSLocalizedString("app_name", comment: "") + " App"
title.textColor = AppTheme.Colors.red
title.font = .preferredFont(forTextStyle: .title3, weight: .semibold)
view.addSubview(title)
title.leadingAnchor.constraint(equalTo: logo.trailingAnchor, constant: 10.0).isActive = true
title.centerYAnchor.constraint(equalTo: logo.centerYAnchor).isActive = true
title.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
return view
}()
private lazy var magnitudeLegendView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
@@ -95,16 +67,11 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
override func extraUI() {
view.addSubview(magnitudeLegendView)
view.addSubview(watermarkView)
magnitudeLegendView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
magnitudeLegendView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
magnitudeLegendView.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
magnitudeLegendView.topAnchor.constraint(equalTo: mapView.topAnchor).isActive = true
watermarkView.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 10.0).isActive = true
watermarkView.leadingAnchor.constraint(equalTo: mapView.leadingAnchor, constant: 10.0).isActive = true
watermarkView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
}
override func configureUI() {
@@ -153,7 +120,7 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
.first
// controlliamo che sia inferiore al raggio impostato per le notifiche
if let radius = Double(EQNNotificheSegnalazioniUtente.shared().distanzaPosizione),
if let radius = Double(EQNSettingUserReportNotification.shared.distanzaMassima),
let nearestCluser = nearestCluser,
abs(nearestCluser.distance(from: userPosition)) < radius {
centerLocation = nearestCluser
@@ -191,33 +158,20 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
@objc private func onTapMapDetailStyleButton(_ sender: Any) {
appPreferences.userReportExpandedView.toggle()
loadDataSource()
reloadMap()
}
@objc private func onTapScreenshotButton(_ sender: Any) {
let snapshot = createSnapshot()
let controller = UIActivityViewController(activityItems: [snapshot], applicationActivities: [])
present(controller, animated: true)
}
public func createSnapshot() -> UIImage {
// mostriamo il watermark e nascondiamo la legenda
watermarkView.isHidden = false
magnitudeLegendView.isHidden = true
// riduciamo la porzione da salvare alla sola mappa (eliminiamo i filtri)
let size = CGSize(width: view.bounds.width, height: mapView.bounds.maxY)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { ctx in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
let screenshot = createSnapshot {
// nascondiamo la legenda
magnitudeLegendView.isHidden = true
} restore: {
// ri-visualizziamo la legenda
magnitudeLegendView.isHidden = false
}
// torniamo allo stato originale
watermarkView.isHidden = true
magnitudeLegendView.isHidden = false
return image
let controller = UIActivityViewController(activityItems: [screenshot], applicationActivities: [])
present(controller, animated: true)
}
// MARK: - Private
@@ -233,10 +187,10 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
var cluster_code = 0
var vector_cluster = [Int](repeating: 0, count: vector_latitude.count)
for i in 0..<vector_latitude.count {
let deltaMinute_i = getDeltaMinute(vector_date[i])
let deltaMinute_i = EQNUtility.getDeltaMinute(vector_date[i])
if vector_cluster[i] == 0 && deltaMinute_i <= minutes {
for j in 0..<vector_latitude.count {
let deltaMinute_j = getDeltaMinute(vector_date[j])
let deltaMinute_j = EQNUtility.getDeltaMinute(vector_date[j])
if i != j && deltaMinute_j <= minutes {
if abs(vector_latitude[i] - vector_latitude[j]) < 4 && abs(vector_longitude[i] - vector_longitude[j]) < 4 && abs(deltaMinute_i - deltaMinute_j) <= 20 {
if vector_cluster[j] > 0 {
@@ -327,10 +281,6 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
mapView.addOverlays(overlays)
}
private func getDeltaMinute(_ date: Date) -> TimeInterval {
Date().timeIntervalSince(date) / 60.0
}
// MARK: - Map
override func setupAnnotationView(for annotation: MKAnnotation, on mapView: MKMapView) -> MKAnnotationView? {
@@ -7,49 +7,100 @@
//
import UIKit
import Shogun
class SegnalazioniSendReportCell: EQNBaseTableViewCell {
class SegnalazioniSendReportCell: EQNBaseContainerTableViewCell {
private struct Report: Equatable {
let magnitude: Int
let text: String
let color: UIColor
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.magnitude == rhs.magnitude
}
}
@objc var onTapReport: (_ magnitude: Int) -> Void = { _ in }
override var headerText: String { NSLocalizedString("main_feel", comment: "") }
// MARK: - UI
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var reportMercalli2: UILabel!
@IBOutlet private weak var reportMercalli3: UILabel!
@IBOutlet private weak var reportMercalli4: UILabel!
@IBOutlet private weak var reportMercalli5: UILabel!
@IBOutlet private weak var reportMercalli6: UILabel!
@IBOutlet private weak var reportMercalli7: UILabel!
@IBOutlet private weak var reportMercalli8: UILabel!
@IBOutlet private weak var reportMercalli9: UILabel!
@IBOutlet private weak var reportMercalli10: UILabel!
@IBOutlet private weak var reportMercalli11: UILabel!
@IBOutlet private weak var reportMercalli12: UILabel!
// MARK: - View Lifecycle
private let reports: [Report] = [
.init(magnitude: 20, text: NSLocalizedString("mercalli_II", comment: ""), color: .init(named: "Mercalli 20")!),
.init(magnitude: 30, text: NSLocalizedString("mercalli_III", comment: ""), color: .init(named: "Mercalli 30")!),
.init(magnitude: 40, text: NSLocalizedString("mercalli_IV", comment: ""), color: .init(named: "Mercalli 40")!),
.init(magnitude: 50, text: NSLocalizedString("mercalli_V", comment: ""), color: .init(named: "Mercalli 50")!),
.init(magnitude: 60, text: NSLocalizedString("mercalli_VI", comment: ""), color: .init(named: "Mercalli 60")!),
.init(magnitude: 70, text: NSLocalizedString("mercalli_VII", comment: ""), color: .init(named: "Mercalli 70")!),
.init(magnitude: 80, text: NSLocalizedString("mercalli_VIII", comment: ""), color: .init(named: "Mercalli 80")!),
.init(magnitude: 90, text: NSLocalizedString("mercalli_IX", comment: ""), color: .init(named: "Mercalli 90")!),
.init(magnitude: 100, text: NSLocalizedString("mercalli_X", comment: ""), color: .init(named: "Mercalli 100")!),
.init(magnitude: 110, text: NSLocalizedString("mercalli_XI", comment: ""), color: .init(named: "Mercalli 110")!),
.init(magnitude: 120, text: NSLocalizedString("mercalli_XII", comment: ""), color: .init(named: "Mercalli 120")!)
]
override func awakeFromNib() {
super.awakeFromNib()
// MARK: - Internal
override func setupUI() {
super.setupUI()
localizeUI()
var previousView = topView
reports.enumerated().forEach { index, report in
let view = createContentView(magnitude: report.magnitude, text: report.text, color: report.color)
containerView.addSubview(view)
view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
let padding: CGFloat = report == reports.first ? .cardPadding : 0
view.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: padding).isActive = true
if report == reports.last {
view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
}
previousView = view
}
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("main_feel", comment: "")
reportMercalli2.text = NSLocalizedString("mercalli_II", comment: "")
reportMercalli3.text = NSLocalizedString("mercalli_III", comment: "")
reportMercalli4.text = NSLocalizedString("mercalli_IV", comment: "")
reportMercalli5.text = NSLocalizedString("mercalli_V", comment: "")
reportMercalli6.text = NSLocalizedString("mercalli_VI", comment: "")
reportMercalli7.text = NSLocalizedString("mercalli_VII", comment: "")
reportMercalli8.text = NSLocalizedString("mercalli_VIII", comment: "")
reportMercalli9.text = NSLocalizedString("mercalli_IX", comment: "")
reportMercalli10.text = NSLocalizedString("mercalli_X", comment: "")
reportMercalli11.text = NSLocalizedString("mercalli_XI", comment: "")
reportMercalli12.text = NSLocalizedString("mercalli_XII", comment: "")
private func createContentView(
magnitude: Int,
text: String,
color: UIColor
) -> UIView {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = color
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .body)
label.textColor = AppTheme.shared.cardTextColor
label.numberOfLines = 0
label.text = text
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .clear
button.tag = magnitude
button.addTarget(self, action: #selector(onTapMagnitudeButton(_:)), for: .touchUpInside)
view.addSubview(label)
view.addSubview(button)
// use a custom vertical spacing to make single lines bigger
let verticalSpacing: CGFloat = 15.0
label.topAnchor.constraint(equalTo: view.topAnchor, constant: verticalSpacing).isActive = true
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: .cardPadding).isActive = true
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: .cardPadding.negative).isActive = true
label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -verticalSpacing).isActive = true
button.constraint(to: view)
return view
}
// MARK: - Actions
@@ -58,4 +109,9 @@ class SegnalazioniSendReportCell: EQNBaseTableViewCell {
let magnitude = sender.tag
onTapReport(magnitude)
}
@objc private func onTapMagnitudeButton(_ sender: UIButton) {
let magnitude = sender.tag
onTapReport(magnitude)
}
}
@@ -38,8 +38,12 @@
- (void)setupUI
{
self.title = [NSLocalizedString(@"tab_manual", nil) capitalizedString];
self.tableView.estimatedRowHeight = 500.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.contentInset = EQNBaseContainerTableViewCell.EdgeInsets;
[self.tableView registerClass:[SegnalazioniLast24HoursCell class] forCellReuseIdentifier:@"Last24HCell"];
[self.tableView registerClass:[SegnalazioniSendReportCell class] forCellReuseIdentifier:@"ReportEarthquakeCell"];
}
- (void)refreshUI
@@ -74,8 +78,17 @@
{
if (indexPath.row == 0) {
SegnalazioniLast24HoursCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Last24HCell" forIndexPath:indexPath];
EQNReteSmartphone *reteSmartPhone = [EQNManager defaultManager].rete_smartphone;
[cell updateUIFor:reteSmartPhone];
[cell updateWith:[EQNManager defaultManager].rete_smartphone];
__weak SegnalazioniViewController *weakSelf = self;
cell.onTapMap = ^{
[weakSelf openMap];
};
cell.onTapTwitter = ^{
[weakSelf openTwitter];
};
cell.onTapTelegram = ^{
[weakSelf openTelegram];
};
return cell;
}
@@ -88,21 +101,21 @@
#pragma mark - Actions
- (IBAction)openMapTapped:(id)sender
- (void)openMap
{
SegnalazioniMapViewController *controller = [[SegnalazioniMapViewController alloc] init];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:controller];
[self presentViewController:navController animated:YES completion:nil];
}
- (IBAction)openTwitterTapped:(id)sender
- (void)openTwitter
{
NSURL *twitterUrl = [NSURL URLWithString:EQNTwitterProfileUrl];
SFSafariViewController *controller = [[SFSafariViewController alloc] initWithURL:twitterUrl];
[self presentViewController:controller animated:YES completion:nil];
}
- (IBAction)openTelegramTapped:(id)sender
- (void)openTelegram
{
NSURL *telegramUrl = [NSURL URLWithString:EQNTelegramUrl];
[[UIApplication sharedApplication] openURL:telegramUrl options:@{} completionHandler:nil];
@@ -28,7 +28,7 @@ class SeismicNetworkAdvertiseTableViewCell: UITableViewCell {
return view
}()
private lazy var bannerView: GADNativeAdView = {
private lazy var bannerView: NativeAdView = {
let view = GADTMediumTemplateView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
@@ -71,7 +71,7 @@ class SeismicNetworkAdvertiseTableViewCell: UITableViewCell {
// MARK: - Public
func loadNativeAd(_ nativeAd: GADNativeAd) {
func loadNativeAd(_ nativeAd: NativeAd) {
bannerView.nativeAd = nativeAd
}
}
@@ -0,0 +1,127 @@
//
// SeismicNetworkBaseTableViewCell.swift
// Earthquake Network
//
// Created by Andrea Busi on 06/03/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import UIKit
import Shogun
protocol SeismicNetworkBaseTableViewCellDelegate: AnyObject {
func seismicNetworkCellDidTapShare(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapIntensityMapDetail(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapCalendar(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapSettings(_ cell: SeismicNetworkBaseTableViewCell)
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkBaseTableViewCell)
}
class SeismicNetworkBaseTableViewCell: UITableViewCell {
/// Delegate
weak var delegate: SeismicNetworkBaseTableViewCellDelegate?
/// Available informations to display inside the cell
enum InformationType: Int {
case preliminary
case time
case distance
case coordinate
case population
case realtimeSmartphones
case reportUsers
case intensityMap
case buttons
}
// MARL: - Internal
static let DefaultButtonHeight: CGFloat = 34.0
static let VerticalSpacingDefault: CGFloat = 6.0
static let VerticalSpacingSmall: CGFloat = 2.0
static let HorizontalSpacingDefault: CGFloat = 4.0
// MARK: - UI Components
lazy var containerView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.clipsToBounds = true
return view
}()
lazy var gradientView: UIImageView = {
// Per gestire il gradiente, utilizziamo una image view in cui inseriamo un'immagine
// creata ad-hoc con il gradiente desiderato.
// Le prove fatte utilizzando una view normale sono fallite perchè al momento di
// disegnare la view non abbiamo le misure corrette.
let view = UIImageView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleToFill
return view
}()
// MARK: - Init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
// MARK: - View Lifecycle
override func layoutSubviews() {
super.layoutSubviews()
containerView.eqn_applyShadowAndRoundedCorners()
gradientView.eqn_applyRoundedCorners()
}
// MARK: - Setup
func setupUI() {
selectionStyle = .default
backgroundColor = .clear
// container view
contentView.addSubview(containerView)
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4.0).isActive = true
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0).isActive = true
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0).isActive = true
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4.0).isActive = true
containerView.addSubview(gradientView)
gradientView.constraint(to: containerView)
}
func recreateUI() {
// remove all subviews and recreate the required components
containerView.subviews.forEach({ $0.removeFromSuperview() })
setupUI()
}
@discardableResult
func addSeparator(constraintTo: NSLayoutYAxisAnchor, constanst: CGFloat = 8.0) -> UIView {
let separator = UIView()
separator.translatesAutoresizingMaskIntoConstraints = false
separator.backgroundColor = .lightGray
containerView.addSubview(separator)
separator.topAnchor.constraint(equalTo: constraintTo, constant: constanst).isActive = true
separator.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
separator.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
separator.heightAnchor.constraint(equalToConstant: 1.0).isActive = true
return separator
}
}
@@ -0,0 +1,233 @@
//
// SeismicNetworkMinimalTableViewCell.swift
// Earthquake Network
//
// Created by Andrea Busi on 06/03/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import UIKit
import Shogun
class SeismicNetworkMinimalTableViewCell: SeismicNetworkBaseTableViewCell {
// MARK: - UI
private lazy var magnitudeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.preferredFont(forTextStyle: .largeTitle)
label.textColor = .red
label.textAlignment = .center
return label
}()
private lazy var placeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.preferredFont(forTextStyle: .title2, weight: .semibold)
label.numberOfLines = 3
return label
}()
private lazy var timeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var distanceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var smartphonesLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
private lazy var alertsLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
// MARK: - Internal
/// Seismic to show
private var seismic: EQNSisma?
private var isPushSelected = false
private var informationTypes: Set<InformationType> = []
// MARK: - Setup
override func setupUI() {
super.setupUI()
// this variable is used to keep track of the previous view, in order to attach proper constraints
var previousView: UIView = containerView
// preliminary banner on top of the cell
if informationTypes.contains(.preliminary) {
let preliminaryLabel = UILabel()
preliminaryLabel.translatesAutoresizingMaskIntoConstraints = false
preliminaryLabel.text = NSLocalizedString("official_prelimiary", comment: "").uppercased()
preliminaryLabel.textAlignment = .center
preliminaryLabel.backgroundColor = .red
preliminaryLabel.textColor = .yellow
containerView.addSubview(preliminaryLabel)
preliminaryLabel.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
preliminaryLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
preliminaryLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
preliminaryLabel.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
previousView = preliminaryLabel
}
containerView.addSubview(magnitudeLabel)
containerView.addSubview(placeLabel)
let titleTopAnchor = previousView == containerView ? containerView.layoutMarginsGuide.topAnchor : previousView.bottomAnchor
let stackViewInformations = UIStackView(arrangedSubviews: [timeLabel, distanceLabel])
stackViewInformations.translatesAutoresizingMaskIntoConstraints = false
stackViewInformations.axis = .horizontal
stackViewInformations.distribution = .fillEqually
stackViewInformations.spacing = Self.HorizontalSpacingDefault
containerView.addSubview(stackViewInformations)
let stackViewRight = UIStackView(arrangedSubviews: [placeLabel, stackViewInformations])
stackViewRight.translatesAutoresizingMaskIntoConstraints = false
stackViewRight.axis = .vertical
stackViewRight.distribution = .equalSpacing
stackViewRight.spacing = Self.VerticalSpacingDefault
let stackViewMain = UIStackView(arrangedSubviews: [magnitudeLabel, stackViewRight])
stackViewMain.translatesAutoresizingMaskIntoConstraints = false
stackViewMain.axis = .horizontal
stackViewMain.distribution = .fill
stackViewMain.spacing = Self.HorizontalSpacingDefault
containerView.addSubview(stackViewMain)
stackViewMain.topAnchor.constraint(equalTo: titleTopAnchor).isActive = true
stackViewMain.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
stackViewMain.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
magnitudeLabel.widthAnchor.constraint(equalToConstant: 60.0).isActive = true
previousView = stackViewMain
if informationTypes.contains(.realtimeSmartphones) || informationTypes.contains(.reportUsers) || informationTypes.contains(.intensityMap) {
let separator = addSeparator(constraintTo: previousView.bottomAnchor, constanst: Self.VerticalSpacingDefault)
let stackViewReports = UIStackView()
stackViewReports.translatesAutoresizingMaskIntoConstraints = false
stackViewReports.axis = .vertical
stackViewReports.distribution = .equalSpacing
stackViewReports.alignment = .center
stackViewReports.spacing = Self.VerticalSpacingDefault
if informationTypes.contains(.realtimeSmartphones) {
stackViewReports.addArrangedSubview(smartphonesLabel)
}
if informationTypes.contains(.reportUsers) {
stackViewReports.addArrangedSubview(alertsLabel)
}
if informationTypes.contains(.intensityMap) {
let buttonMap = EQNRoundedButton.make(title: "🎯 \(NSLocalizedString("shakemap", comment: ""))", target: self, action: #selector(intensityMapTapped(_:)))
stackViewReports.addArrangedSubview(buttonMap)
buttonMap.heightAnchor.constraint(equalToConstant: Self.DefaultButtonHeight).isActive = true
buttonMap.leadingAnchor.constraint(equalTo: stackViewReports.leadingAnchor).isActive = true
buttonMap.trailingAnchor.constraint(equalTo: stackViewReports.trailingAnchor).isActive = true
}
containerView.addSubview(stackViewReports)
stackViewReports.topAnchor.constraint(equalTo: separator.bottomAnchor, constant: Self.VerticalSpacingDefault).isActive = true
stackViewReports.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
stackViewReports.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
previousView = stackViewReports
}
previousView.bottomAnchor.constraint(equalTo: containerView.layoutMarginsGuide.bottomAnchor).isActive = true
containerView.eqn_applyShadowAndRoundedCorners()
gradientView.eqn_applyRoundedCorners()
}
private func updateUI() {
guard let seismic = seismic else { return }
let viewModel = SeismicNetworkMinimalViewModel(seismic: seismic)
gradientView.image = .gradient(from: viewModel.colors.startColor, to: viewModel.colors.endColor, with: .init(origin: .zero, size: .init(width: 500, height: 1)))
placeLabel.text = viewModel.place
placeLabel.textColor = isPushSelected ? AppTheme.Colors.pureBlue : AppTheme.shared.cardTextColor
magnitudeLabel.textColor = viewModel.colors.textColor
magnitudeLabel.text = viewModel.magnitude
timeLabel.text = "🕗 \(viewModel.time)"
distanceLabel.text = "📐 \(viewModel.distance)"
if !viewModel.smartphones.isEmpty {
smartphonesLabel.text = "🚨 \(viewModel.smartphones)"
}
if !viewModel.users.isEmpty {
alertsLabel.text = "⚠️ \(viewModel.users)"
}
}
// MARK: - Public
/// Configure the cell to display a seismic
/// - Parameters:
/// - seismic: Seismic to display
/// - type: Type of cell
/// - informations: Informations to show
public func configure(
with seismic: EQNSisma,
isPushSelected: Bool
) {
self.seismic = seismic
self.isPushSelected = isPushSelected
self.informationTypes.removeAll()
if seismic.preliminary.intValue > 0 {
informationTypes.insert(.preliminary)
}
if seismic.smartphoneNumber.intValue > 0 {
informationTypes.insert(.realtimeSmartphones)
}
if seismic.userNumber.intValue > 0 {
informationTypes.insert(.reportUsers)
}
if seismic.isoCode != "0" {
informationTypes.insert(.intensityMap)
}
recreateUI()
updateUI()
}
// MARK: - Actions
@objc private func intensityMapTapped(_ sender: Any) {
delegate?.seismicNetworkCellDidTapIntensityMapDetail(self)
}
}
@@ -11,32 +11,8 @@ import MapKit
import CoreLocation
import Shogun
protocol SeismicNetworkTableViewCellDelegate: AnyObject {
func seismicNetworkCellDidTapShare(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapCalendar(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapSettings(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkTableViewCell)
}
class SeismicNetworkTableViewCell: UITableViewCell {
static let Identifier = "SeismicNetworkTableViewCell"
typealias MagnitudeColors = (textColor: UIColor, startColor: UIColor, endColor: UIColor)
/// Available informations to display inside the cell
enum InformationType: Int {
case preliminary
case time
case distance
case coordinate
case population
case realtimeSmartphones
case reportUsers
case buttons
}
class SeismicNetworkTableViewCell: SeismicNetworkBaseTableViewCell {
/// Available cell type
enum DisplayType {
@@ -45,45 +21,15 @@ class SeismicNetworkTableViewCell: UITableViewCell {
/// Cell with map visible
case mapExpanded
}
/// Delegate
weak var delegate: SeismicNetworkTableViewCellDelegate?
// MARK: - Internal
private static let DefaultVerticalSpacing: CGFloat = 6.0
private static let DefaultBodyFont = UIFont.preferredFont(forTextStyle: .body)
private static let DefaultBodyFontLight = UIFont.preferredFont(forTextStyle: .body, weight: .light)
/// Seismic to show
private var seismic: EQNSisma?
private(set) var displayType = DisplayType.normal
private var informationTypes = [InformationType]()
private var colors: MagnitudeColors?
private var isPushSelected = false
// MARK: - UI Components
private lazy var containerView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = AppTheme.shared.cardCornerRadius
view.layer.masksToBounds = false
// add shadow
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.5
view.layer.shadowOffset = CGSize(width: 0, height: 2)
view.layer.shadowRadius = 2
return view
}()
private lazy var titleImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var placeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
@@ -92,11 +38,20 @@ class SeismicNetworkTableViewCell: UITableViewCell {
return label
}()
private lazy var shareButton: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(named: "share_icon"), for: .normal)
button.addTarget(self, action: #selector(shareTapped(_:)), for: .touchUpInside)
return button
}()
private lazy var networkLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = UIColor.white.withAlphaComponent(0.5)
label.textAlignment = .center
label.textAlignment = .right
label.font = .preferredFont(forTextStyle: .subheadline)
label.numberOfLines = 2
return label
}()
@@ -111,35 +66,40 @@ class SeismicNetworkTableViewCell: UITableViewCell {
private lazy var depthLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Self.DefaultBodyFontLight
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var timeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Self.DefaultBodyFontLight
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var distanceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Self.DefaultBodyFontLight
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var coordinateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Self.DefaultBodyFontLight
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
private lazy var populationLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Self.DefaultBodyFontLight
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 2
return label
}()
@@ -147,8 +107,9 @@ class SeismicNetworkTableViewCell: UITableViewCell {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = Self.DefaultBodyFont
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
@@ -156,8 +117,9 @@ class SeismicNetworkTableViewCell: UITableViewCell {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = Self.DefaultBodyFont
label.font = .preferredFont(forTextStyle: .body)
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
@@ -183,18 +145,19 @@ class SeismicNetworkTableViewCell: UITableViewCell {
setupUI()
}
// MARK: - View Lifecycle
override func layoutSubviews() {
super.layoutSubviews()
containerView.eqn_applyShadowAndRoundedCorners()
gradientView.eqn_applyRoundedCorners()
}
// MARK: - Setup
private func setupUI() {
selectionStyle = .default
backgroundColor = .clear
// container view
contentView.addSubview(containerView)
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4.0).isActive = true
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0).isActive = true
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0).isActive = true
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4.0).isActive = true
override func setupUI() {
super.setupUI()
// this variable is used to keep track of the previous view, in order to attach proper constraints
var previousView: UIView = containerView
@@ -216,48 +179,27 @@ class SeismicNetworkTableViewCell: UITableViewCell {
previousView = preliminaryLabel
}
// title (bell icon, place label, seismic network and share button)
let titleComponentsHeight: CGFloat = 30.0
let stackViewTitle = UIStackView()
stackViewTitle.translatesAutoresizingMaskIntoConstraints = false
stackViewTitle.axis = .horizontal
stackViewTitle.distribution = .fill
stackViewTitle.alignment = .center
stackViewTitle.spacing = 4
let shareButton = UIButton(type: .custom)
shareButton.setImage(UIImage(named: "share_icon"), for: .normal)
shareButton.addTarget(self, action: #selector(shareTapped(_:)), for: .touchUpInside)
stackViewTitle.addArrangedSubview(titleImageView)
stackViewTitle.addArrangedSubview(placeLabel)
stackViewTitle.addArrangedSubview(networkLabel)
stackViewTitle.addArrangedSubview(shareButton)
titleImageView.heightAnchor.constraint(equalToConstant: titleComponentsHeight).isActive = true
titleImageView.widthAnchor.constraint(equalTo: titleImageView.heightAnchor).isActive = true
networkLabel.heightAnchor.constraint(equalToConstant: 34.0).isActive = true
networkLabel.setContentHuggingPriority(.init(800), for: .horizontal)
networkLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
placeLabel.setContentHuggingPriority(.init(200), for: .horizontal)
placeLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
shareButton.widthAnchor.constraint(equalToConstant: titleComponentsHeight).isActive = true
shareButton.widthAnchor.constraint(equalTo: shareButton.heightAnchor).isActive = true
containerView.addSubview(placeLabel)
containerView.addSubview(shareButton)
let titleTopAnchor = previousView == containerView ? containerView.layoutMarginsGuide.topAnchor : previousView.bottomAnchor
containerView.addSubview(stackViewTitle)
stackViewTitle.topAnchor.constraint(equalTo: titleTopAnchor).isActive = true
stackViewTitle.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
stackViewTitle.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
placeLabel.topAnchor.constraint(equalTo: titleTopAnchor).isActive = true
placeLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
placeLabel.trailingAnchor.constraint(equalTo: shareButton.leadingAnchor, constant: .cardPadding.negative).isActive = true
shareButton.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
shareButton.centerYAnchor.constraint(equalTo: placeLabel.centerYAnchor).isActive = true
shareButton.heightAnchor.constraint(equalToConstant: 24.0).isActive = true
shareButton.heightAnchor.constraint(equalTo: shareButton.widthAnchor, multiplier: 1.0).isActive = true
let separator1 = addSeparator(constraintTo: stackViewTitle.bottomAnchor)
let separator1 = addSeparator(constraintTo: placeLabel.bottomAnchor)
let informationsLeadingAnchor = separator1.leadingAnchor
let informationsTrailingAnchor = separator1.trailingAnchor
// magnitude information
containerView.addSubview(magnitudeLabel)
magnitudeLabel.topAnchor.constraint(equalTo: separator1.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
magnitudeLabel.topAnchor.constraint(equalTo: separator1.bottomAnchor, constant: Self.VerticalSpacingSmall).isActive = true
magnitudeLabel.leadingAnchor.constraint(equalTo: informationsLeadingAnchor, constant: 14).isActive = true
if !informationTypes.contains(.preliminary) {
@@ -287,20 +229,27 @@ class SeismicNetworkTableViewCell: UITableViewCell {
}
containerView.addSubview(stackViewInformations)
stackViewInformations.topAnchor.constraint(equalTo: magnitudeLabel.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
stackViewInformations.topAnchor.constraint(equalTo: magnitudeLabel.bottomAnchor, constant: Self.VerticalSpacingSmall).isActive = true
stackViewInformations.leadingAnchor.constraint(equalTo: informationsLeadingAnchor, constant: 14).isActive = true
stackViewInformations.trailingAnchor.constraint(equalTo: informationsTrailingAnchor, constant: -14).isActive = true
previousView = stackViewInformations
if informationTypes.contains(.realtimeSmartphones) || informationTypes.contains(.reportUsers) {
let separator2 = addSeparator(constraintTo: stackViewInformations.bottomAnchor)
// network
containerView.addSubview(networkLabel)
networkLabel.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.VerticalSpacingSmall).isActive = true
networkLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
networkLabel.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
previousView = networkLabel
if informationTypes.contains(.realtimeSmartphones) || informationTypes.contains(.reportUsers) || informationTypes.contains(.intensityMap) {
let separator2 = addSeparator(constraintTo: previousView.bottomAnchor, constanst: Self.VerticalSpacingSmall)
let stackViewReports = UIStackView()
stackViewReports.translatesAutoresizingMaskIntoConstraints = false
stackViewReports.axis = .vertical
stackViewReports.distribution = .equalSpacing
stackViewReports.alignment = .center
stackViewReports.spacing = Self.DefaultVerticalSpacing
stackViewReports.spacing = Self.VerticalSpacingDefault
if informationTypes.contains(.realtimeSmartphones) {
stackViewReports.addArrangedSubview(smartphonesLabel)
@@ -308,34 +257,43 @@ class SeismicNetworkTableViewCell: UITableViewCell {
if informationTypes.contains(.reportUsers) {
stackViewReports.addArrangedSubview(alertsLabel)
}
if informationTypes.contains(.intensityMap) {
let buttonMap = EQNRoundedButton.make(title: "🎯 \(NSLocalizedString("shakemap", comment: ""))", target: self, action: #selector(intensityMapTapped(_:)))
stackViewReports.addArrangedSubview(buttonMap)
buttonMap.heightAnchor.constraint(equalToConstant: Self.DefaultButtonHeight).isActive = true
buttonMap.leadingAnchor.constraint(equalTo: stackViewReports.leadingAnchor).isActive = true
buttonMap.trailingAnchor.constraint(equalTo: stackViewReports.trailingAnchor).isActive = true
}
containerView.addSubview(stackViewReports)
stackViewReports.topAnchor.constraint(equalTo: separator2.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
stackViewReports.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor, constant: 20.0).isActive = true
stackViewReports.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor, constant: -20.0).isActive = true
stackViewReports.topAnchor.constraint(equalTo: separator2.bottomAnchor, constant: Self.VerticalSpacingDefault).isActive = true
stackViewReports.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
stackViewReports.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
let separator3 = addSeparator(constraintTo: stackViewReports.bottomAnchor)
previousView = separator3
previousView = stackViewReports
}
if informationTypes.contains(.buttons) {
let separator3 = addSeparator(constraintTo: previousView.bottomAnchor)
previousView = separator3
// buttons
let stackViewButtons = UIStackView()
stackViewButtons.translatesAutoresizingMaskIntoConstraints = false
stackViewButtons.axis = .horizontal
stackViewButtons.distribution = .fillEqually
stackViewButtons.spacing = 4
stackViewButtons.spacing = 8
let buttonMap = createRoundedButton(title: "🗺", action: #selector(mapTapped(_:)))
let buttonMap = EQNRoundedButton.make(title: "🗺", target: self, action: #selector(mapTapped(_:)))
stackViewButtons.addArrangedSubview(buttonMap)
let buttonCalendar = createRoundedButton(title: "📆", action: #selector(calendarTapped(_:)))
let buttonCalendar = EQNRoundedButton.make(title: "📆", target: self, action: #selector(calendarTapped(_:)))
stackViewButtons.addArrangedSubview(buttonCalendar)
let buttonSettings = createRoundedButton(title: "🔧", action: #selector(settingsTapped(_:)))
let buttonSettings = EQNRoundedButton.make(title: "🔧", target: self, action: #selector(settingsTapped(_:)))
stackViewButtons.addArrangedSubview(buttonSettings)
containerView.addSubview(stackViewButtons)
stackViewButtons.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
stackViewButtons.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
stackViewButtons.heightAnchor.constraint(equalToConstant: Self.DefaultButtonHeight).isActive = true
stackViewButtons.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.VerticalSpacingDefault).isActive = true
stackViewButtons.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
stackViewButtons.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
@@ -345,7 +303,7 @@ class SeismicNetworkTableViewCell: UITableViewCell {
if displayType == .mapExpanded {
containerView.addSubview(mapView)
mapView.heightAnchor.constraint(equalToConstant: 140.0).isActive = true
mapView.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
mapView.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.VerticalSpacingDefault).isActive = true
mapView.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
@@ -353,10 +311,11 @@ class SeismicNetworkTableViewCell: UITableViewCell {
}
if (displayType == .mapExpanded) {
let buttonClose = createRoundedButton(title: NSLocalizedString("official_close", comment: "").uppercased(), action: #selector(closeTapped(_:)))
let buttonClose = EQNRoundedButton.make(title: NSLocalizedString("official_close", comment: "").uppercased(), target: self, action: #selector(closeTapped(_:)))
containerView.addSubview(buttonClose)
buttonClose.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
buttonClose.heightAnchor.constraint(equalToConstant: Self.DefaultButtonHeight).isActive = true
buttonClose.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.VerticalSpacingDefault).isActive = true
buttonClose.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
buttonClose.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
buttonClose.bottomAnchor.constraint(equalTo: containerView.layoutMarginsGuide.bottomAnchor).isActive = true
@@ -364,12 +323,9 @@ class SeismicNetworkTableViewCell: UITableViewCell {
else {
previousView.bottomAnchor.constraint(equalTo: containerView.layoutMarginsGuide.bottomAnchor).isActive = true
}
}
private func recreateUI() {
// remove all subviews and recreate the required components
containerView.subviews.forEach({ $0.removeFromSuperview() })
setupUI()
containerView.eqn_applyShadowAndRoundedCorners()
gradientView.eqn_applyRoundedCorners()
}
private func updateUI() {
@@ -377,15 +333,13 @@ class SeismicNetworkTableViewCell: UITableViewCell {
let viewModel = SeismicNetworkViewModel(seismic: seismic)
containerView.backgroundColor = colors?.startColor
let notified = couldBeNotified(for: seismic)
titleImageView.image = notified ? UIImage(named: "bell") : UIImage(named: "bell_disabled")
gradientView.image = .gradient(from: viewModel.colors.startColor, to: viewModel.colors.endColor, with: .init(origin: .zero, size: .init(width: 500, height: 1)))
// update seismic data
placeLabel.text = viewModel.place
networkLabel.text = viewModel.network + " " // add some padding
magnitudeLabel.textColor = colors?.textColor
placeLabel.textColor = isPushSelected ? AppTheme.Colors.pureBlue : AppTheme.shared.cardTextColor
networkLabel.text = String(format: NSLocalizedString("official_provider", comment: ""), viewModel.network)
magnitudeLabel.textColor = viewModel.colors.textColor
magnitudeLabel.text = viewModel.magnitude
depthLabel.text = viewModel.depth
timeLabel.text = "🕗 \(viewModel.time)"
@@ -404,7 +358,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
alertsLabel.text = "⚠️ \(viewModel.users)"
}
if displayType == .mapExpanded {
// zoom based on population involved
let longitudeSpan = mapSpanLongitude(population: seismic.population100km)
@@ -431,11 +384,16 @@ class SeismicNetworkTableViewCell: UITableViewCell {
/// - seismic: Seismic to display
/// - type: Type of cell
/// - informations: Informations to show
public func configure(with seismic: EQNSisma, type: DisplayType, informations: [InformationType]) {
public func configure(
with seismic: EQNSisma,
type: DisplayType,
informations: [InformationType],
isPushSelected: Bool
) {
self.seismic = seismic
self.colors = calculateColors(for: seismic.magnitude.doubleValue)
self.displayType = type
self.informationTypes = informations
self.isPushSelected = isPushSelected
if !informations.contains(.time) {
self.informationTypes += [.time]
@@ -450,6 +408,9 @@ class SeismicNetworkTableViewCell: UITableViewCell {
if seismic.userNumber.intValue > 0 && !informations.contains(.reportUsers) {
self.informationTypes += [.reportUsers]
}
if seismic.isoCode == "0" && informations.contains(.intensityMap) {
self.informationTypes.removeAll { $0 == .intensityMap }
}
recreateUI()
updateUI()
@@ -457,90 +418,38 @@ class SeismicNetworkTableViewCell: UITableViewCell {
// MARK: - Actions
@objc func shareTapped(_ sender: UIButton) {
@objc private func shareTapped(_ sender: UIButton) {
delegate?.seismicNetworkCellDidTapShare(self)
}
@objc func mapTapped(_ sender: UIButton) {
@objc private func mapTapped(_ sender: UIButton) {
if displayType != .mapExpanded {
delegate?.seismicNetworkCellDidTapMap(self)
}
}
@objc func calendarTapped(_ sender: UIButton) {
@objc private func calendarTapped(_ sender: UIButton) {
delegate?.seismicNetworkCellDidTapCalendar(self)
}
@objc func settingsTapped(_ sender: UIButton) {
@objc private func settingsTapped(_ sender: UIButton) {
delegate?.seismicNetworkCellDidTapSettings(self)
}
@objc func closeTapped(_ sender: UIButton) {
@objc private func closeTapped(_ sender: UIButton) {
delegate?.seismicNetworkCellDidTapClose(self)
}
@objc func mapDetailTapped(_ sender: Any) {
@objc private func mapDetailTapped(_ sender: Any) {
delegate?.seismicNetworkCellDidTapMapDetail(self)
}
@objc private func intensityMapTapped(_ sender: Any) {
delegate?.seismicNetworkCellDidTapIntensityMapDetail(self)
}
// MARK: - Helpers
@discardableResult
private func addSeparator(constraintTo: NSLayoutYAxisAnchor, constanst: CGFloat = 8.0) -> UIView {
let separator = UIView()
separator.translatesAutoresizingMaskIntoConstraints = false
separator.backgroundColor = .lightGray
containerView.addSubview(separator)
separator.topAnchor.constraint(equalTo: constraintTo, constant: constanst).isActive = true
separator.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
separator.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
separator.heightAnchor.constraint(equalToConstant: 1.0).isActive = true
return separator
}
private func createRoundedButton(title: String, action: Selector) -> EQNRoundedButton {
let button = EQNRoundedButton(frame: .zero)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: action, for: .touchUpInside)
button.setTitle(title, for: .normal)
button.setTitleColor(AppTheme.Colors.darkGray, for: .normal)
button.backgroundColor = UIColor.white.withAlphaComponent(0.5)
return button
}
/// Check if the user could be received a notification for this seismic
private func couldBeNotified(for seismic: EQNSisma) -> Bool {
let settings = EQNNotificheReteSismiche.shared()
if !settings.isAbilitato {
return false
}
if !settings.listaEnti.contains(seismic.provider) {
return false
}
var notified = true
if let radius = Double(settings.distanzaPosizione), seismic.userDistance > radius {
notified = false
}
if let magnitude = Double(settings.energiaSisma), seismic.magnitude.doubleValue < magnitude {
notified = false
}
if settings.isAbilitaVicini, seismic.userDistance < 50 {
notified = true
}
if settings.isTerremortiForti, let strongMagnitude = Double(settings.energiaTerremotiForti), seismic.magnitude.doubleValue >= strongMagnitude {
notified = true
}
return notified
}
/// Determines the zoom for the map, based on the involved population
private func mapSpanLongitude(population: Double) -> CLLocationDegrees {
var zoom: CLLocationDegrees = 1
@@ -553,55 +462,4 @@ class SeismicNetworkTableViewCell: UITableViewCell {
}
return zoom
}
/// Calculate colors to use for text and background of the cell
private func calculateColors(for magnitude: Double) -> MagnitudeColors {
var textColor = UIColor.black
var r = 0, g = 0, b = 0
if (magnitude < 2.0) {
let fraction: Double = 1 - (magnitude - 0.0) / (2.0 - 0.0)
r = Int(round(200.0 + (255.0 - 200.0) * fraction))
g = Int(round(226.0 + (255.0 - 226.0) * fraction))
b = Int(round(196.0 + (255.0 - 196.0) * fraction))
textColor = UIColor(red: 12.0 / 255.0, green: 115.0 / 255.0, blue: 160.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 2.0 && magnitude < 3.5) {
let fraction: Double = 1 - (magnitude - 2) / (3.5 - 2)
r = Int(round(136.0 + (200.0 - 136.0) * fraction))
g = Int(round(175.0 + (226.0 - 175.0) * fraction))
b = Int(round(131.0 + (196.0 - 131.0) * fraction))
textColor = UIColor(red: 12.0 / 255.0, green: 160.0 / 255.0, blue: 35.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 3.5 && magnitude < 4.5) {
let fraction: Double = 1 - (magnitude - 3.5) / (4.5 - 3.5)
r = 252
g = Int(round(233.0 + (253.0 - 233.0) * fraction))
b = Int(round(179.0 + (209.0 - 179.0) * fraction))
textColor = UIColor(red: 244.0 / 255.0, green: 195.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 4.5 && magnitude < 5.5) {
let fraction: Double = 1 - (magnitude - 4.5) / (5.5 - 4.5)
r = 252
g = Int(round(159.0 + (197.0 - 159.0) * fraction))
b = Int(round(161.0 + (197.0 - 161.0) * fraction))
textColor = UIColor(red: 255.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 5.5) {
let fraction: Double = 1 - (magnitude - 5.5) / (10 - 5.5)
r = Int(round(190.0 + (254.0 - 190.0) * fraction))
g = Int(round(124.0 + (219.0 - 124.0) * fraction))
b = 255
textColor = UIColor(red: 183.0 / 255.0, green: 60.0 / 255.0, blue: 252.0 / 255.0, alpha: 1.0)
}
let r2 = min(r + 30, 255)
let g2 = min(g + 30, 255)
let b2 = min(b + 30, 255)
let startColor = UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: 1.0)
let endColor = UIColor(red: CGFloat(r2) / 255.0, green: CGFloat(g2) / 255.0, blue: CGFloat(b2) / 255.0, alpha: 1.0)
return (textColor: textColor, startColor: startColor, endColor: endColor)
}
}
@@ -1,48 +0,0 @@
//
// FiltersViewModel.swift
// Earthquake Network
//
// Created by Andrea Busi on 22/03/21.
// Copyright © 2021 Earthquake Network. All rights reserved.
//
import Foundation
struct FiltersViewModel {
let magnitude: String
let distance: String
let timeframe: String
init() {
let magnitudoMinima = EQNData.magitudoDebole(for: EQNSeismic.shared.magnitudoMinima)
self.magnitude = Self.formattedMagnitude(magnitudoMinima.value)
let distanzaMassima = EQNData.raggioSisma(for: EQNSeismic.shared.distanzaMassima)
self.distance = Self.formattedDistance(distanzaMassima.value)
let periodoTemporale = EQNData.periodoTemporale(for: EQNSeismic.shared.periodoTemporale)
self.timeframe = Self.formattedTimeframe(periodoTemporale.value)
}
// MARK: - Private
private static func formattedMagnitude(_ magnitude: String) -> String {
return magnitude
}
private static func formattedDistance(_ distance: String) -> String {
if distance == EQNData.MaxRaggioSisma {
return ""
}
return "\(distance)km"
}
private static func formattedTimeframe(_ timeframe: String) -> String {
let time = Int(timeframe) ?? 0
if time < 60 {
return "\(time)m"
}
return "\(time/60)h"
}
}
@@ -16,13 +16,12 @@ protocol SeismicFiltersViewControllerDelegate: AnyObject {
class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private enum RowIdentifier: Int {
case magnitudoMinima
case sismiNelRaggio
case distanzaMassima
case periodoTemporale
case sismiFortiAbilita
case sismiFortiDistanza
case sismiQualsiasiMagnitudo
case modificaImpostazioni
case magnitudoMinima
case sismiRilevanti
case sismiTutti
case sismiPercepiti
}
weak var delegate: SeismicFiltersViewControllerDelegate?
@@ -37,29 +36,21 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
@IBOutlet private weak var closeButton: UIButton!
private var settings = [
SettingItem(type: .slider, title: NSLocalizedString("filter_magnitude", comment: "")),
SettingItem(type: .slider, title: NSLocalizedString("filter_distance", comment: "")),
SettingItem(type: .slider, title: NSLocalizedString("filter_timeframe", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_strong", comment: "")),
SettingItem(type: .slider, title: NSLocalizedString("options_strong_magnitude", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_near", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_reflect", comment: ""))
SettingItem(type: .enable, title: NSLocalizedString("filter_show_area", comment: "")),
SettingItem(type: .slider, title: ""),
SettingItem(type: .slider, title: NSLocalizedString("filter_minimum_magnitude", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_show_relevant", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_show_all", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_show_felt", comment: ""))
]
private let dataSourceMagnitudoMinima = EQNData.magitudoDeboli()
private let dataSourceDistanzaMassima = EQNData.raggioSismi()
private let dataSourcePeriodoTemporale = EQNData.periodiTemporali()
private let dataSourceSismiForti = EQNData.magitudoForti()
private var initialMagnitudoMinima: EQNGenericValue?
private var initialQualsiasiMagnitudo: Bool?
private let initialFilterType = EQNSeismic.shared.filterOption
private(set) var currentFilterType = EQNSeismic.FilterType.inRadius
private var currentMaximumDistance = EQNData.DefaultFilterRadius
private var currentMinimumMagnitude = EQNData.DefaultFilterMagnitude
private var currentMagnitudoMinima = EQNData.DefaultMagitudoDebole
private var currentDistanzaMassima = EQNData.DefaultRaggioSisma
private var currentPeriodoTemporale = EQNData.DefaultPeriodoTemporale
private var currentSismiFortiAbilitati = false
private var currentSismiFortiDistanza = EQNData.DefaultMagitudoForte
private var currentSismiQualsiasiMagnitudo = false
private var currentModificaImpostazioni = false
private let dataSourceMaximumDistance = EQNData.filterRadius
private let dataSourceMinimumMagnitude = EQNData.filterMagnitude
// MARK: - View Lifecycle
@@ -86,19 +77,9 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
}
private func loadDataSource() {
currentMagnitudoMinima = EQNData.magitudoDebole(for: EQNSeismic.shared.magnitudoMinima)
if initialMagnitudoMinima == nil {
initialMagnitudoMinima = currentMagnitudoMinima
}
currentDistanzaMassima = EQNData.raggioSisma(for: EQNSeismic.shared.distanzaMassima)
currentPeriodoTemporale = EQNData.periodoTemporale(for: EQNSeismic.shared.periodoTemporale)
currentSismiFortiAbilitati = EQNSeismic.shared.sismiFortiAbilitati
currentSismiFortiDistanza = EQNData.magitudoForte(for: EQNSeismic.shared.sismiFortiMagnitudo)
currentSismiQualsiasiMagnitudo = EQNSeismic.shared.sismiQualsiasiAbilitati
if initialQualsiasiMagnitudo == nil {
initialQualsiasiMagnitudo = currentSismiQualsiasiMagnitudo
}
currentModificaImpostazioni = EQNSeismic.shared.modificaImpostazioniAbilitato
currentFilterType = EQNSeismic.shared.filterOption
currentMaximumDistance = EQNData.filterRadius(for: EQNSeismic.shared.maximumDistance)
currentMinimumMagnitude = EQNData.filterMagnitude(for: EQNSeismic.shared.minimumMagnitude)
}
// MARK: - Table view delegate and data source
@@ -108,45 +89,36 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let identifier = RowIdentifier(rawValue: indexPath.row) else {
return UITableViewCell()
}
let setting = settings[indexPath.row]
let isLocationAvailable = EQNUser.default().lastPosition != nil
switch setting.type {
case .slider:
let cell = SettingSliderTableViewCell(style: .default, reuseIdentifier: nil)
cell.titleLabel.text = setting.displayTitle
if indexPath.row == RowIdentifier.magnitudoMinima.rawValue {
cell.configureSlider(with: dataSourceMagnitudoMinima, current: currentMagnitudoMinima)
cell.valueChanged = { [unowned self] value in
currentMagnitudoMinima = value
EQNSeismic.shared.magnitudoMinima = value.value
EQNSeismic.shared.saveFilters()
let isFilterInRadiusEnabled = currentFilterType == .inRadius && isLocationAvailable
switch identifier {
case .distanzaMassima:
cell.isDisabled = !isFilterInRadiusEnabled
cell.isUserInteractionEnabled = isFilterInRadiusEnabled
cell.configureSlider(with: dataSourceMaximumDistance, current: currentMaximumDistance)
cell.valueChanged = { [weak self] value in
self?.onChangeMaximumDistance(value)
}
cell.dragEnded = { [unowned self] in
showWarningAlertIfNeeded(for: currentMagnitudoMinima)
}
} else if indexPath.row == RowIdentifier.distanzaMassima.rawValue {
cell.configureSlider(with: dataSourceDistanzaMassima, current: currentDistanzaMassima)
cell.valueChanged = { [unowned self] value in
currentDistanzaMassima = value
EQNSeismic.shared.distanzaMassima = value.value
EQNSeismic.shared.saveFilters()
}
} else if indexPath.row == RowIdentifier.periodoTemporale.rawValue {
cell.configureSlider(with: dataSourcePeriodoTemporale, current: currentPeriodoTemporale)
cell.valueChanged = { [unowned self] value in
currentPeriodoTemporale = value
EQNSeismic.shared.periodoTemporale = value.value
EQNSeismic.shared.saveFilters()
}
} else if indexPath.row == RowIdentifier.sismiFortiDistanza.rawValue {
cell.isDisabled = !currentSismiFortiAbilitati
cell.configureSlider(with: dataSourceSismiForti, current: currentSismiFortiDistanza)
cell.valueChanged = { [unowned self] value in
currentSismiFortiDistanza = value
EQNSeismic.shared.sismiFortiMagnitudo = value.value
EQNSeismic.shared.saveFilters()
case .magnitudoMinima:
cell.isDisabled = !isFilterInRadiusEnabled
cell.isUserInteractionEnabled = isFilterInRadiusEnabled
cell.configureSlider(with: dataSourceMinimumMagnitude, current: currentMinimumMagnitude)
cell.valueChanged = { [weak self] value in
self?.onChangeMinimumMagnitude(value)
}
default:
break
}
return cell
@@ -155,30 +127,37 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
cell.titleLabel.text = setting.displayTitle
cell.detailTextLabel?.text = setting.subtitle
if indexPath.row == RowIdentifier.sismiFortiAbilita.rawValue {
cell.toggleSwitch.isOn = currentSismiFortiAbilitati
cell.valueChanged = { [unowned self] value in
currentSismiFortiAbilitati = value
EQNSeismic.shared.sismiFortiAbilitati = value
EQNSeismic.shared.saveFilters()
loadDataSource()
tableView.reloadData()
switch identifier {
case .sismiNelRaggio:
let isCurrentFilter = currentFilterType == .inRadius
cell.isDisabled = !isLocationAvailable
cell.toggleSwitch.isOn = isCurrentFilter
cell.valueChanged = { [weak self] enabled in
self?.onChangeFilterOption(enabled, filter: .inRadius)
}
} else if indexPath.row == RowIdentifier.sismiQualsiasiMagnitudo.rawValue {
cell.toggleSwitch.isOn = currentSismiQualsiasiMagnitudo
cell.valueChanged = { [unowned self] value in
currentSismiQualsiasiMagnitudo = value
EQNSeismic.shared.sismiQualsiasiAbilitati = value
EQNSeismic.shared.saveFilters()
cell.errorLabel.text = !isLocationAvailable ? NSLocalizedString("filter_nolocation", comment: "") : nil
case .sismiRilevanti:
let isCurrentFilter = currentFilterType == .positionRelevant
cell.isDisabled = !isLocationAvailable
cell.toggleSwitch.isOn = isCurrentFilter
cell.valueChanged = { [weak self] enabled in
self?.onChangeFilterOption(enabled, filter: .positionRelevant)
}
} else if indexPath.row == RowIdentifier.modificaImpostazioni.rawValue {
cell.toggleSwitch.isOn = currentModificaImpostazioni
cell.valueChanged = { [unowned self] value in
currentModificaImpostazioni = value
EQNSeismic.shared.modificaImpostazioniAbilitato = value
EQNSeismic.shared.saveFilters()
cell.errorLabel.text = !isLocationAvailable ? NSLocalizedString("filter_nolocation", comment: "") : nil
case .sismiTutti:
let isCurrentFilter = currentFilterType == .worldWide
cell.toggleSwitch.isOn = isCurrentFilter
cell.valueChanged = { [weak self] enabled in
self?.onChangeFilterOption(enabled, filter: .worldWide)
}
case .sismiPercepiti:
let isCurrentFilter = currentFilterType == .userFelt
cell.toggleSwitch.isOn = isCurrentFilter
cell.valueChanged = { [weak self] enabled in
self?.onChangeFilterOption(enabled, filter: .userFelt)
}
default:
break
}
return cell
@@ -191,41 +170,34 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
@IBAction func exitTapped(_ sender: UIButton) {
// data needs to be re-downloaded if (or conditions):
// a) new magnitude is lower than the previous one and new value is less than 2.0
// b) show any near earthquake is active and value is changed
if let initialMagnitude = Float(initialMagnitudoMinima?.value ?? "10.0"), let currentMagnitude = Float(currentMagnitudoMinima.value) {
needsDataUpdate = currentMagnitude < 2.0 && initialMagnitude > currentMagnitude
}
if let initialQualsiasiMagnitudo = initialQualsiasiMagnitudo, currentSismiQualsiasiMagnitudo == true, initialQualsiasiMagnitudo != currentSismiQualsiasiMagnitudo {
needsDataUpdate = true
}
// a) filter type is changed
needsDataUpdate = initialFilterType != currentFilterType
delegate?.seismicFiltersControllerDidUpdateFilters(self)
updateNotificationSettingsIfNeeded()
dismiss(animated: true, completion: nil)
}
// MARK: - Private
private func showWarningAlertIfNeeded(for value: EQNGenericValue) {
guard let magnitude = Double(value.value), magnitude < 2.0 else { return }
private func onChangeFilterOption(_ enabled: Bool, filter: EQNSeismic.FilterType) {
currentFilterType = filter
EQNSeismic.shared.filterOption = filter
EQNSeismic.shared.saveFilters()
let alert = UIAlertController(title: NSLocalizedString("attention", comment: ""), message: NSLocalizedString("options_low_magnitude", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("main_understood", comment: ""), style: .default, handler: nil))
present(alert, animated: true, completion: nil)
loadDataSource()
tableView.reloadData()
}
private func onChangeMaximumDistance(_ item: EQNGenericValue) {
currentMaximumDistance = item
EQNSeismic.shared.maximumDistance = item.value
EQNSeismic.shared.saveFilters()
}
private func updateNotificationSettingsIfNeeded() {
// if the switch is enabled, update also the settings notification
guard currentModificaImpostazioni == true else { return }
// update notification settings with current filters
EQNNotificheReteSismiche.shared().energiaSisma = EQNSeismic.shared.magnitudoMinima;
EQNNotificheReteSismiche.shared().distanzaPosizione = EQNSeismic.shared.distanzaMassima
EQNNotificheReteSismiche.shared().isAbilitaVicini = EQNSeismic.shared.sismiQualsiasiAbilitati
EQNNotificheReteSismiche.shared().isTerremortiForti = EQNSeismic.shared.sismiFortiAbilitati
EQNNotificheReteSismiche.shared().energiaTerremotiForti = EQNSeismic.shared.sismiFortiMagnitudo
private func onChangeMinimumMagnitude(_ item: EQNGenericValue) {
currentMinimumMagnitude = item
EQNSeismic.shared.minimumMagnitude = item.value
EQNSeismic.shared.saveFilters()
}
}
@@ -29,17 +29,16 @@ class SeismicCardSettingsViewController: UIViewController {
@IBOutlet private weak var informationPopulationSwitch: UISwitch!
@IBOutlet private weak var closeButton: UIButton!
private var informations = [SeismicNetworkTableViewCell.InformationType]()
private var informations: [SeismicNetworkTableViewCell.InformationType] {
get { AppPreferences.shared.seismicNetworksInformations }
set { AppPreferences.shared.seismicNetworksInformations = newValue }
}
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
if let saved = UserDefaults.standard.array(forKey: EQNUserDefaultKeySesmicInformations) as? [Int] {
informations = saved.compactMap { SeismicNetworkTableViewCell.InformationType(rawValue: $0) }
}
setupUI()
updateUI()
}
@@ -84,7 +83,6 @@ class SeismicCardSettingsViewController: UIViewController {
toggle(information: .population)
}
UserDefaults.standard.set(informations.map { $0.rawValue }, forKey: EQNUserDefaultKeySesmicInformations)
updateUI()
}
@@ -0,0 +1,9 @@
//
// SeismicNetworkData.swift
// Earthquake Network
//
// Created by Andrea Busi on 31/01/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import Foundation
@@ -0,0 +1,144 @@
//
// SeismicNetworkFilterRecapView.swift
// Earthquake Network
//
// Created by Andrea Busi on 17/07/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import SwiftUI
import Shogun
struct SeismicNetworkFilterRecapView: View {
class Model: ObservableObject {
@Published var filter = EQNSeismic.shared.filterOption
@Published var sort = EQNSeismic.shared.sort
}
@ObservedObject private var model: Model
private let onSort: (_ sort: EQNSeismic.Sort) -> Void
private let onMainFilter: () -> Void
private let onMap: () -> Void
// MARK: - Init
init(
model: Model,
onSort: @escaping (_ sort: EQNSeismic.Sort) -> Void,
onMainFilter: @escaping () -> Void,
onMap: @escaping () -> Void
) {
self.model = model
self.onSort = onSort
self.onMainFilter = onMainFilter
self.onMap = onMap
}
// MARK: - View
var body: some View {
HStack(spacing: 0) {
RoundedButton(
systemName: sortIcon,
tintColor: tintColor,
action: {
model.sort.advance()
onSort(model.sort)
}
)
Spacer()
Button {
onMainFilter()
} label: {
HStack {
Image(systemName: "magnifyingglass")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 12)
Text(filterTitle)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay {
Capsule().stroke(AppTheme.Colors.gray.color, lineWidth: 1)
}
}
.font(.caption)
.tint(tintColor)
Spacer()
RoundedButton(
systemName: "globe",
tintColor: tintColor,
action: onMap
)
}
.frame(maxWidth: .infinity)
.background(Color.clear)
}
private var tintColor: Color {
Color.blue
//AppTheme.Colors.lightBlue.color
}
private var filterTitle: String {
switch model.filter {
case .inRadius: "filter_area".localized
case .positionRelevant: "filter_relevant".localized
case .worldWide: "filter_all".localized
case .userFelt: "filter_felt".localized
}
}
private var sortIcon: String {
switch model.sort {
case .time: "clock"
case .position:
if #available(iOS 16, *) {
"compass.drawing"
} else {
"ruler"
}
case .magnitude: "thermometer"
}
}
}
private struct RoundedButton: View {
let systemName: String
let tintColor: Color
let action: () -> Void
var body: some View {
Button {
action()
} label: {
Image(systemName: systemName)
.resizable()
.tint(tintColor)
.aspectRatio(contentMode: .fit)
.frame(maxHeight: .infinity)
.frame(width: 40.0)
.padding(8)
.overlay {
Capsule().stroke(AppTheme.Colors.gray.color, lineWidth: 1)
}
.animation(nil, value: systemName) // previene animazioni implicite nel bottone
}
}
}
#Preview {
SeismicNetworkFilterRecapView(
model: .init(),
onSort: { _ in },
onMainFilter: {},
onMap: {}
)
.frame(height: 34.0)
}
@@ -0,0 +1,108 @@
//
// SeismicNetworkScrollIndicatorView.swift
// Earthquake Network
//
// Created by Andrea Busi on 31/01/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import UIKit
import CoreGraphics
class SeismicNetworkScrollIndicatorView: UIView {
private static let HighlightColor: UIColor = .red
var seismics: [SeismicNetworkViewModel] = [] {
didSet {
setNeedsDisplay()
}
}
var highlighted: SeismicNetworkViewModel? {
didSet {
setNeedsDisplay()
}
}
private var numberOfRectangles: Int {
seismics.count
}
// MARK: - View Lifecycle
override func draw(_ rect: CGRect) {
guard numberOfRectangles > 0 else { return }
let context = UIGraphicsGetCurrentContext()
let rectStandardWidth = rect.width
let rectStandardHeight = rect.height / CGFloat(numberOfRectangles)
let rectHighlightedMinHeight: CGFloat = 4
let smallRectangles = rectStandardHeight < 10
let highlightIndex = seismics.firstIndex(where: { $0 == highlighted }) ?? 100_000
seismics.enumerated().forEach { index, seismic in
// Disegniamo un rettangolo per ogni sisma, quello evidenziato deve avere un contorno rosso.
// Ci sono situazioni in cui ci sono molti sismi da mostrare, quindi in quel caso facciamo alcune modifiche:
// - usiamo un'altezza minima per il sisma evidenziato
// - per il sisma evidenziato, anche il contenuto è rosso (e non solo il bordo)
// - negli altri sismi, non mostriamo il bordo
if highlightIndex == index {
// Stiamo disegnando il sisma evidenziato.
// Valutiamo se utilizzare l'altezza minima.
let rectHeight = smallRectangles ? rectHighlightedMinHeight : rectStandardHeight
let yPosition = CGFloat(index) * rectStandardHeight
let rectangle = CGRect(x: 0, y: yPosition, width: rectStandardWidth, height: rectHeight)
let fillColor = smallRectangles ? Self.HighlightColor : seismic.colors.textColor.withAlphaComponent(0.3)
context?.setFillColor(fillColor.cgColor)
context?.fill(rectangle)
if !smallRectangles {
// disegniamo il bordo solo se i rettangoli non sono piccoli
let borderWidth: CGFloat = 2.0
context?.setStrokeColor(Self.HighlightColor.cgColor)
context?.setLineWidth(borderWidth) // Spessore del bordo
context?.stroke(rectangle.insetBy(dx: borderWidth / 2, dy: borderWidth / 2)) // Evita che il bordo venga tagliato
}
} else {
// Stiamo disegnando i sismi non evidenziati, utilizziamo sempre l'altezza predefinita
// Dobbiamo eventualmente calcolare un offset aggiuntivo,
// perchè il sisma evidenziato ha un'altezza maggiore (se i rettangoli sono piccoli)
let rectHeight = rectStandardHeight
var offset: CGFloat = 0
if index > highlightIndex && smallRectangles {
// calcoliamo l'offset prima del rettangolo evidenziato
let preOffset = CGFloat(highlightIndex - 1) * rectStandardHeight
// offset diverso dovuto all'altezza diversa del rettangolo evidenziato
let highlightOffset = rectHighlightedMinHeight
// calcoliamo l'offset tra il rettangolo evidenziato e quello corrente
let postOffset = CGFloat(index - highlightIndex) * rectStandardHeight
offset = preOffset + highlightOffset + postOffset
} else {
// siamo prima del rettangolo evidenziato, non abbiamo calcoli da fare
offset = CGFloat(index) * rectHeight
}
let rectangle = CGRect(x: 0, y: offset, width: rectStandardWidth, height: rectHeight)
let fillColor = seismic.colors.textColor.withAlphaComponent(0.3)
context?.setFillColor(fillColor.cgColor)
context?.fill(rectangle)
if !smallRectangles {
// altrimenti un bordo grigio
let borderWidth: CGFloat = 0.5
context?.setStrokeColor(AppTheme.Colors.gray.cgColor)
context?.setLineWidth(borderWidth) // Spessore del bordo
context?.stroke(rectangle)
}
}
}
}
}
@@ -8,24 +8,75 @@
import Foundation
struct MagnitudeColors {
let textColor: UIColor
let startColor: UIColor
let endColor: UIColor
}
struct SeismicNetworkMinimalViewModel {
private let seismic: EQNSisma
let place: String
let isPreliminary: Bool
let magnitude: String
let time: String
let distance: String
let smartphones: String
let users: String
let colors: MagnitudeColors
// MARK: - Init
init(seismic: EQNSisma) {
self.seismic = seismic
self.place = seismic.place
let isPreliminary = seismic.preliminary.intValue > 0
self.isPreliminary = isPreliminary
self.magnitude = String(format: "%.1f", seismic.magnitude.doubleValue)
let time = EQNUtility.formattedString(forTimeDifference: Int(seismic.timeDifference))
self.time = time
let distanceRounded = Int(round(seismic.userDistance))
self.distance = "\(distanceRounded) km"
if seismic.smartphoneNumber.intValue > 0 {
self.smartphones = String(format: NSLocalizedString("official_smartphones", comment: ""), seismic.smartphoneNumber)
} else {
self.smartphones = ""
}
if seismic.userNumber.intValue > 0 {
self.users = String(format: NSLocalizedString("official_reports", comment: ""), seismic.userNumber)
} else {
self.users = ""
}
self.colors = calculateColors(for: seismic.magnitude.doubleValue)
}
}
struct SeismicNetworkViewModel {
var place: String
var network: String
var isPreliminary: Bool
var magnitude: String
var depth: String
var time: String
var distance: String
var coordinate: String
var population: String
var smartphones: String
var users: String
private let seismic: EQNSisma
let place: String
let network: String
let isPreliminary: Bool
let magnitude: String
let depth: String
let time: String
let distance: String
let coordinate: String
let population: String
let smartphones: String
let users: String
let colors: MagnitudeColors
// MARK: - Init
init(seismic: EQNSisma) {
self.seismic = seismic
self.place = seismic.place
self.network = seismic.provider
@@ -38,7 +89,7 @@ struct SeismicNetworkViewModel {
self.depth = ""
} else {
self.magnitude = String(format: "%.1f%@", seismic.magnitude.doubleValue, seismic.magnitudeType)
self.depth = String(format: "%@ %.1f km", NSLocalizedString("official_depth", comment: ""), seismic.depth.doubleValue)
self.depth = String(format: "%.1f km", seismic.depth.doubleValue)
}
// we need to check agains null values, because sometimes WS returns invalid dates
@@ -56,7 +107,7 @@ struct SeismicNetworkViewModel {
let coordinateText = EQNUtility.coordinateString(coordinate: seismic.coordinate.coordinate)
self.coordinate = "\(coordinateText)"
let population = Self.formatPopulation(seismic.population100km)
let population = formatPopulation(seismic.population100km)
self.population = String(format: NSLocalizedString("share_radius100", comment: ""), population)
if seismic.smartphoneNumber.intValue > 0 {
@@ -69,23 +120,82 @@ struct SeismicNetworkViewModel {
} else {
self.users = ""
}
}
// MARK: - Private
/// Format population value (ex. 1.5M, 2.4k)
private static func formatPopulation(_ population: Double) -> String {
var populationString = ""
if population > 999_999 {
let roundedPopulation = round(population / 100_000) / 10
populationString = "\(roundedPopulation)M"
} else if population > 999 {
let roundedPopulation = round(population / 100) / 10
populationString = "\(roundedPopulation)K"
} else {
let roundedPopulation = round(population)
populationString = "\(roundedPopulation)"
}
return populationString
self.colors = calculateColors(for: seismic.magnitude.doubleValue)
}
}
extension SeismicNetworkViewModel: Equatable {
static func == (lhs: SeismicNetworkViewModel, rhs: SeismicNetworkViewModel) -> Bool {
return lhs.seismic == rhs.seismic
}
}
// MARK: - Helpers
/// Calculate colors to use for text and background of the cell
private func calculateColors(for magnitude: Double) -> MagnitudeColors {
var textColor = UIColor.black
var r = 0, g = 0, b = 0
if (magnitude < 2.0) {
let fraction: Double = 1 - (magnitude - 0.0) / (2.0 - 0.0)
r = Int(round(200.0 + (255.0 - 200.0) * fraction))
g = Int(round(226.0 + (255.0 - 226.0) * fraction))
b = Int(round(196.0 + (255.0 - 196.0) * fraction))
textColor = UIColor(red: 12.0 / 255.0, green: 115.0 / 255.0, blue: 160.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 2.0 && magnitude < 3.5) {
let fraction: Double = 1 - (magnitude - 2) / (3.5 - 2)
r = Int(round(136.0 + (200.0 - 136.0) * fraction))
g = Int(round(175.0 + (226.0 - 175.0) * fraction))
b = Int(round(131.0 + (196.0 - 131.0) * fraction))
textColor = UIColor(red: 12.0 / 255.0, green: 160.0 / 255.0, blue: 35.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 3.5 && magnitude < 4.5) {
let fraction: Double = 1 - (magnitude - 3.5) / (4.5 - 3.5)
r = 252
g = Int(round(233.0 + (253.0 - 233.0) * fraction))
b = Int(round(179.0 + (209.0 - 179.0) * fraction))
textColor = UIColor(red: 244.0 / 255.0, green: 195.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 4.5 && magnitude < 5.5) {
let fraction: Double = 1 - (magnitude - 4.5) / (5.5 - 4.5)
r = 252
g = Int(round(159.0 + (197.0 - 159.0) * fraction))
b = Int(round(161.0 + (197.0 - 161.0) * fraction))
textColor = UIColor(red: 255.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0)
}
if (magnitude >= 5.5) {
let fraction: Double = 1 - (magnitude - 5.5) / (10 - 5.5)
r = Int(round(190.0 + (254.0 - 190.0) * fraction))
g = Int(round(124.0 + (219.0 - 124.0) * fraction))
b = 255
textColor = UIColor(red: 183.0 / 255.0, green: 60.0 / 255.0, blue: 252.0 / 255.0, alpha: 1.0)
}
let r2 = min(r + 30, 255)
let g2 = min(g + 30, 255)
let b2 = min(b + 30, 255)
let startColor = UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: 1.0)
let endColor = UIColor(red: CGFloat(r2) / 255.0, green: CGFloat(g2) / 255.0, blue: CGFloat(b2) / 255.0, alpha: 1.0)
return .init(textColor: textColor, startColor: startColor, endColor: endColor)
}
/// Format population value (ex. 1.5M, 2.4k)
private func formatPopulation(_ population: Double) -> String {
var populationString = ""
if population > 999_999 {
let roundedPopulation = round(population / 100_000) / 10
populationString = "\(roundedPopulation)M"
} else if population > 999 {
let roundedPopulation = round(population / 100) / 10
populationString = "\(roundedPopulation)K"
} else {
let roundedPopulation = round(population)
populationString = "\(roundedPopulation)"
}
return populationString
}
@@ -0,0 +1,270 @@
//
// 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.advance()
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 = intensityString(from: shakemap.intensity)
annotationView.magnitudeTextColor = intensityTextColor ?? .black
annotationView.magnitudeBackgroundColor = intensityColor
annotationView.canShowCallout = true
return annotationView
case .circle:
return nil
}
}
private func intensityString(from intensity: Float) -> String {
let intensityRounded = (intensity * 10).rounded() / 10
let intensityFloor = floor(intensityRounded)
let romanNumerals: [Int: String] = [
1: "I", 2: "II", 3: "III", 4: "IV",
5: "V", 6: "VI", 7: "VII", 8: "VIII",
9: "IX", 10: "X", 11: "XI", 12: "XII"
]
var result = romanNumerals[Int(intensityFloor)] ?? ""
if intensityRounded != intensityFloor {
let reminder = Int(((intensityRounded - intensityFloor) * 10).rounded())
result += ".\(reminder)"
}
return result
}
}
fileprivate class ShakemapPolyline: MKPolyline {
var intensity: Float = 0
var intensityColor: UIColor = .white
}
@@ -15,11 +15,18 @@ protocol SeismicNetworksMapDetailViewControllerDelegate: AnyObject {
}
class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
private var pinStyle: MapPinStyle {
get { AppPreferences.shared.mapPinStyle }
set { AppPreferences.shared.mapPinStyle = newValue }
}
private let eqnSeismic = EQNSeismic.shared
// MARK: - State
override var isCloseButtonVisible: Bool { false }
override var isFilterViewVisible: Bool {
// a custom filter view id displayed
// a custom filter view is displayed
true
}
weak var delegate: SeismicNetworksMapDetailViewControllerDelegate?
@@ -42,10 +49,7 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
seismicsFilterLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
seismicsFilterLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8.0).isActive = true
seismicsFilterLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8.0).isActive = true
// tap recognizer
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(filtersTapped(_:)))
view.addGestureRecognizer(tapRecognizer)
return view
}()
@@ -59,14 +63,17 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
// MARK: - Internal
private let seismic: EQNSisma
private let seismic: EQNSisma?
private var allSeismics: [EQNSisma]
/// Contains circles drawed on the map
private var mapCircles = [MKCircle]()
// MARK: - Init
init(seismic: EQNSisma, allSeismics: [EQNSisma]) {
init(
seismic: EQNSisma?,
allSeismics: [EQNSisma]
) {
self.seismic = seismic
self.allSeismics = allSeismics
super.init()
@@ -76,6 +83,24 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
fatalError("init(coder:) is not available, please use init(seismic:allSeismics:)")
}
// MARK: - View Lifecycle
override func configureUI() {
super.configureUI()
navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: .init(handler: { [weak self] _ in
self?.dismiss(animated: true)
}))
navigationItem.rightBarButtonItems = [
UIBarButtonItem(image: UIImage(named: "navbar-icon-screenshot"), primaryAction: .init(handler: { [weak self] _ in
self?.shareScreenshot()
})),
UIBarButtonItem(image: UIImage(named: "navbar-icon-pin-arrow"), primaryAction: .init(handler: { [weak self] _ in
self?.nextPinStyle()
})),
]
}
// MARK: - Public
func updateSeismics(_ seismics: [EQNSisma]) {
@@ -84,7 +109,9 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
}
override func registerMapAnnotationViews() {
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.DoubleLineIdentifier)
mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .full))
mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .light))
mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .circle))
}
override func loadDataSource() {
@@ -92,19 +119,23 @@ 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()
}
override func elaborateMapCenter() {
setMapCenter(for: seismic.coordinate)
if let seismic {
setMapCenter(for: seismic.coordinate)
} else if let location = CLLocationManager().location {
setMapCenter(for: location)
}
}
override func didTapAnnotation(_ annotation: MKAnnotation) {
@@ -127,31 +158,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.advance()
reloadMap()
}
private func loadFiltersRecap() {
let filter = EQNSeismic.shared.filterOption
let text = switch filter {
case .inRadius: NSLocalizedString("filter_area", comment: "")
case .positionRelevant: NSLocalizedString("filter_relevant", comment: "")
case .worldWide: NSLocalizedString("filter_all", comment: "")
case .userFelt: NSLocalizedString("filter_felt", comment: "")
}
seismicsFilterLabel.text = text
}
private func addCircle(
center location: CLLocation?,
radius: Double
) {
// remove any previous circles
mapView.removeOverlays(mapCircles)
mapCircles.removeAll()
// aggiungiamo 3 cerchi concentrici per il punto specificato (se disponibile)
// li inseriamo a a 50, 100 e 200 km.
guard let location = location else { return }
let circles = [
MKCircle(center: location.coordinate, radius: 25_000),
MKCircle(center: location.coordinate, radius: 100_000),
MKCircle(center: location.coordinate, radius: 200_000)
MKCircle(center: location.coordinate, radius: radius),
]
// !!note: is important to assign here the circles
@@ -162,16 +223,13 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
mapView.addOverlays(circles)
}
// MARK: - Actions
@objc override func filtersTapped(_ sender: UIGestureRecognizer) {
let controller = SeismicFiltersViewController.makeViewController()
controller.delegate = self
controller.modalPresentationStyle = .overCurrentContext
controller.modalTransitionStyle = .crossDissolve
present(controller, animated: true, completion: nil)
private func shareScreenshot() {
let screenshot = createSnapshot()
let controller = UIActivityViewController(activityItems: [screenshot], applicationActivities: [])
present(controller, animated: true)
}
// MARK: - Map
override func setupAnnotationView(for annotation: MKAnnotation, on mapView: MKMapView) -> MKAnnotationView? {
@@ -179,15 +237,8 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
return nil
}
let viewModel = SeismicNetworkViewModel(seismic: annotation.seismic)
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: EQNCustomAnnotationView.DoubleLineIdentifier, for: annotation) as! EQNCustomAnnotationView
annotationView.image = annotation.image
annotationView.title = annotation.title
annotationView.subtitle = viewModel.magnitude
return annotationView
let isUserSelection = annotation.seismic == seismic
return annotation.toAnnotationView(mapView: mapView, style: pinStyle, isUserSelection: isUserSelection)
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
@@ -196,12 +247,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)
@@ -7,6 +7,7 @@
//
import UIKit
import SwiftUI
import EventKitUI
import DZNEmptyDataSet
import Shogun
@@ -15,23 +16,23 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
private enum CellType {
case seismic(EQNSisma)
case advertise(GADNativeAd)
case advertise(NativeAd)
}
enum CardDisplayType: Int, CaseIterable {
case small
case full
case minimal
}
private static let SegueIdentifierFilters = "ShowFilters"
private static let SegueIdentifierSettings = "ShowSettings"
private static let SegueIdentifierSeismicNetworks = "ShowSeismicNetworks"
private static let SegueIdentifierCardSettings = "ShowCardSettings"
// MARK: - Internal
@IBOutlet private weak var tableView: UITableView?
@IBOutlet private weak var expandeCollapseButton: UIBarButtonItem!
weak var currentMapController: SeismicNetworksMapDetailViewController?
/// The ad loader
private lazy var adLoader: GADAdLoader = {
let adLoader = GADAdLoader(
private lazy var adLoader: AdLoader = {
let adLoader = AdLoader(
adUnitID: EQNAdMobAppIdNativeBanner, rootViewController: self,
adTypes: [.native], options: nil)
adLoader.delegate = self
@@ -40,25 +41,113 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
/// Cells to display (must be seismics or ad banners)
private var rows = [CellType]()
/// Type of cards to show
private var cardDisplayType: CardDisplayType {
get { AppPreferences.shared.seismicNetworksCardStyle }
set { AppPreferences.shared.seismicNetworksCardStyle = newValue }
}
private var seismicViewModels = [SeismicNetworkViewModel]()
/// Informations to display on a single cell
private var informations = [SeismicNetworkTableViewCell.InformationType]()
private var informations: [SeismicNetworkTableViewCell.InformationType] {
get { AppPreferences.shared.seismicNetworksInformations }
set { AppPreferences.shared.seismicNetworksInformations = newValue }
}
/// Index path of row with map expanded
private var openMapIndexPath: IndexPath?
/// Push notification opened by the user
private var openedPushNotification: EQNOfficialPushNotification? {
didSet {
scrollToOpenedSeismic = true
}
}
private var scrollToOpenedSeismic = false
/// Current displayed controller with map
private weak var currentMapController: SeismicNetworksMapDetailViewController?
/// Keep track of the current cell at the center
private var currentCenteredIndexPath: IndexPath?
// MARK: - UI
@IBOutlet private weak var displayModeButton: UIBarButtonItem!
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
}()
// model is needed in order to send data to SwiftUI view
private let model = SeismicNetworkFilterRecapView.Model()
private lazy var filterRecapView: UIView = {
let hosting = UIHostingController(
rootView: SeismicNetworkFilterRecapView(
model: model,
onSort: { [weak self] sort in
self?.changeSort(to: sort)
},
onMainFilter: { [weak self] in
self?.openFilter()
},
onMap: { [weak self] in
self?.showMapDetail(for: nil)
}
)
)
addChild(hosting)
let filterRecapView = hosting.view!
hosting.view.isOpaque = false
hosting.view.translatesAutoresizingMaskIntoConstraints = false
hosting.didMove(toParent: self)
return hosting.view
}()
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
configureUI()
checkForLocation()
refreshUI()
// only the first time, show the popup for country selection
let alreadyPresented = UserDefaults.standard.bool(forKey: EQNUserDefaultKeyOneShotShowCountry)
if !alreadyPresented {
performSegue(withIdentifier: Self.SegueIdentifierSettings, sender: nil)
UserDefaults.standard.setValue(true, forKey: EQNUserDefaultKeyOneShotShowCountry)
}
configureFilterView(isVisible: false)
NotificationCenter.default.addObserver(self, selector: #selector(didReceiveDownloadCompleteNotification(_:)), name: .EQNDownloadDataDidComplete, object: nil)
}
@@ -67,16 +156,110 @@ 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.backgroundColor = tableView.backgroundColor
view.addSubview(scrollIndicatorView)
view.addSubview(tableView)
view.addSubview(filterRecapView)
scrollIndicatorView.topAnchor.constraint(equalTo: tableView.topAnchor).isActive = true
scrollIndicatorView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollIndicatorView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor).isActive = true
scrollIndicatorView.widthAnchor.constraint(equalToConstant: 10.0).isActive = true
tableViewTopConstraint = tableView.topAnchor.constraint(equalTo: view.topAnchor)
tableViewTopConstraint?.isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: scrollIndicatorView.leadingAnchor).isActive = true
tableView.bottomAnchor
.constraint(
equalTo: filterRecapView.topAnchor,
constant: -8
).isActive = true
filterRecapView.heightAnchor.constraint(equalToConstant: 34.0).isActive = true
filterRecapView.leadingAnchor
.constraint(
equalTo: view.leadingAnchor,
constant: 10.0
).isActive = true
filterRecapView.trailingAnchor
.constraint(
equalTo: view.trailingAnchor,
constant: -10.0
).isActive = true
filterRecapView.topAnchor
.constraint(equalTo: tableView.bottomAnchor).isActive = true
filterRecapView.bottomAnchor
.constraint(
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -8
).isActive = true
}
private func setupFilterView(isVisible: Bool) {
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
}
private func checkForLocation() {
// check if a valid location is available,
// otherwise change the filter settings
if !isLocationAvailable() {
updateFilter(type: .worldWide)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
@@ -85,14 +268,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
@@ -102,15 +277,22 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
}
}
private func showMapDetail(for seismic: EQNSisma) {
private func showMapDetail(for seismic: EQNSisma?) {
let seismics = getSeismics()
let controller = SeismicNetworksMapDetailViewController(seismic: seismic, allSeismics: seismics)
controller.delegate = self
present(controller, animated: true, completion: nil)
let navController = UINavigationController(rootViewController: controller)
present(navController, animated: true, completion: nil)
self.currentMapController = controller
}
private func showIntensityMap(for seismic: EQNSisma) {
let controller = SeismicNetworksIntensityMapViewController(seismic: seismic)
let navController = UINavigationController(rootViewController: controller)
present(navController, animated: true)
}
// MARK: - Notifications
@objc func didReceiveDownloadCompleteNotification(_ sender: Notification) {
@@ -126,21 +308,26 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
private func refreshUI() {
elaborateData()
if let saved = UserDefaults.standard.array(forKey: EQNUserDefaultKeySesmicInformations) as? [Int] {
informations = saved.compactMap { SeismicNetworkTableViewCell.InformationType(rawValue: $0) }
switch cardDisplayType {
case .small:
displayModeButton.image = UIImage(systemName: "1.square")
case .full:
displayModeButton.image = UIImage(systemName: "2.square")
case .minimal:
displayModeButton.image = UIImage(systemName: "3.square")
}
if informations.contains(.buttons) {
expandeCollapseButton.image = UIImage(named: "navbar-icon-arrow-collapse")
} else {
expandeCollapseButton.image = UIImage(named: "navbar-icon-arrow-expand")
}
tableView.reloadData()
updateCenterCellIndexPath()
tableView?.reloadData()
if scrollToOpenedSeismic, let index = getSeismics().firstIndex(where: { isSeismicToHighlight(seismic: $0) }) {
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
scrollToOpenedSeismic = false
}
}
private func loadAd() {
adLoader.load(GADRequest())
adLoader.load(Request())
}
private func loadData(forced: Bool) {
@@ -152,7 +339,8 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
let allSeismics = EQNManager.manager().retiSismiche
let filteredSeismics = EQNSeismic.shared.filterSeismicList(allSeismics ?? [])
rows = filteredSeismics.map { .seismic($0) }
seismicViewModels = filteredSeismics.map(SeismicNetworkViewModel.init)
#if ADS_ENABLED
// if is not a pro user, show an advertise
if !EQNPurchaseUtility.isProVersionEnabled() {
@@ -164,6 +352,9 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
if let mapController = currentMapController {
mapController.updateSeismics(filteredSeismics)
}
scrollIndicatorView.seismics = seismicViewModels
currentCenteredIndexPath = nil
}
private func getSeismics() -> [EQNSisma] {
@@ -176,28 +367,343 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
return seismics
}
private func changeSort(to sort: EQNSeismic.Sort) {
updateFilter(sort: sort)
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,
sort: EQNSeismic.Sort? = nil,
radius: Double? = nil,
magnitude: Double? = nil
) {
if let type {
let previous = EQNSeismic.shared.filterOption
if previous == .userFelt && previous != type {
loadData(forced: true)
}
EQNSeismic.shared.filterOption = type
model.filter = type
}
if let sort {
EQNSeismic.shared.sort = sort
model.sort = sort
}
if let radius {
EQNSeismic.shared.maximumDistance = String(format: "%.0f", radius)
}
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
if rows.count > centerIndexPath.row {
let row = rows[centerIndexPath.row]
if case .seismic = row, seismicViewModels.count > centerIndexPath.row {
scrollIndicatorView.highlighted = seismicViewModels[centerIndexPath.row]
} else {
scrollIndicatorView.highlighted = nil
}
} else {
scrollIndicatorView.highlighted = nil
}
}
}
// MARK: - Actions
@IBAction func refreshDataTapped(_ sender: Any) {
loadData(forced: true)
}
@IBAction func openFilterTapped(_ sender: Any) {
private func openFilter() {
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.advance()
switch cardDisplayType {
case .small:
informations.removeAll(where: { $0 == .buttons })
} else {
case .full:
informations.append(.buttons)
case .minimal:
break
}
UserDefaults.standard.set(informations.map { $0.rawValue }, forKey: EQNUserDefaultKeySesmicInformations)
refreshUI()
}
@@ -211,18 +717,28 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
let row = rows[indexPath.row]
switch row {
case .seismic(let seismic):
let cell = tableView.dequeueReusableCell(withIdentifier: SeismicNetworkTableViewCell.Identifier, for: indexPath) as! SeismicNetworkTableViewCell
var type = SeismicNetworkTableViewCell.DisplayType.normal
if openMapIndexPath == indexPath {
type = .mapExpanded
switch cardDisplayType {
case .small, .full:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SeismicNetworkTableViewCell.self, for: indexPath)
var type = SeismicNetworkTableViewCell.DisplayType.normal
if openMapIndexPath == indexPath {
type = .mapExpanded
}
let isPushSelected = isSeismicToHighlight(seismic: seismic)
cell.configure(with: seismic, type: type, informations: informations, isPushSelected: isPushSelected)
cell.delegate = self
return cell
case .minimal:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SeismicNetworkMinimalTableViewCell.self, for: indexPath)
let isPushSelected = isSeismicToHighlight(seismic: seismic)
cell.configure(with: seismic, isPushSelected: isPushSelected)
cell.delegate = self
return cell
}
cell.configure(with: seismic, type: type, informations: informations)
cell.delegate = self
return cell
case .advertise(let nativeAd):
let cell = tableView.dequeueReusableCell(withIdentifier: SeismicNetworkAdvertiseTableViewCell.Identifier, for: indexPath) as! SeismicNetworkAdvertiseTableViewCell
let cell = tableView.dequeueReusableCell(cellIdentifiable: SeismicNetworkAdvertiseTableViewCell.self, for: indexPath)
cell.loadNativeAd(nativeAd)
return cell
}
@@ -237,6 +753,12 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
}
}
// MARK: - UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateCenterCellIndexPath()
}
// MARK: - Private
private func openCalendar(for seismic: EQNSisma) {
@@ -292,25 +814,25 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
}
}
extension SeismicNetworksViewController: GADNativeAdLoaderDelegate {
func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADNativeAd) {
print("[GADAdLoader] didReceive")
extension SeismicNetworksViewController: NativeAdLoaderDelegate {
func adLoader(_ adLoader: AdLoader, didReceive nativeAd: NativeAd) {
print("[AdLoader] didReceive")
let adPosition = min(3, rows.count)
rows.insert(.advertise(nativeAd), at: adPosition)
tableView?.reloadData()
tableView.reloadData()
}
func adLoader(_ adLoader: GADAdLoader, didFailToReceiveAdWithError error: Error) {
func adLoader(_ adLoader: AdLoader, didFailToReceiveAdWithError error: Error) {
// nope
print("[GADAdLoader] didFailToReceiveAdWithError: \(error.localizedDescription)")
print("[AdLoader] didFailToReceiveAdWithError: \(error.localizedDescription)")
}
}
extension SeismicNetworksViewController: SeismicNetworkTableViewCellDelegate {
extension SeismicNetworksViewController: SeismicNetworkBaseTableViewCellDelegate {
func seismicNetworkCellDidTapShare(_ cell: SeismicNetworkTableViewCell) {
guard let index = tableView?.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
func seismicNetworkCellDidTapShare(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
// create a snapshot of the cell and share with default share sheet
let snapshot = cell.contentView.createSnapshot()
@@ -326,38 +848,44 @@ extension SeismicNetworksViewController: SeismicNetworkTableViewCellDelegate {
present(controller, animated: true)
}
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkTableViewCell) {
guard let index = tableView?.indexPath(for: cell) else { return }
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell) else { return }
let indexToReloads = [openMapIndexPath, index].compactMap { $0 }
openMapIndexPath = index
tableView?.reloadRows(at: indexToReloads, with: .automatic)
tableView.reloadRows(at: indexToReloads, with: .automatic)
}
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkTableViewCell) {
guard let index = tableView?.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
showMapDetail(for: seismic)
}
func seismicNetworkCellDidTapCalendar(_ cell: SeismicNetworkTableViewCell) {
guard let index = tableView?.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
func seismicNetworkCellDidTapIntensityMapDetail(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
showIntensityMap(for: seismic)
}
func seismicNetworkCellDidTapCalendar(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
openCalendar(for: seismic)
}
func seismicNetworkCellDidTapSettings(_ cell: SeismicNetworkTableViewCell) {
func seismicNetworkCellDidTapSettings(_ cell: SeismicNetworkBaseTableViewCell) {
performSegue(withIdentifier: Self.SegueIdentifierCardSettings, sender: nil)
}
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkTableViewCell) {
guard let index = tableView?.indexPath(for: cell) else { return }
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkBaseTableViewCell) {
guard let index = tableView.indexPath(for: cell) else { return }
let indexToReloads = [openMapIndexPath, index].compactMap { $0 }
openMapIndexPath = nil
tableView?.reloadRows(at: indexToReloads, with: .automatic)
tableView.reloadRows(at: indexToReloads, with: .automatic)
}
}
@@ -370,27 +898,12 @@ extension SeismicNetworksViewController: EKEventEditViewDelegate {
extension SeismicNetworksViewController: SeismicFiltersViewControllerDelegate {
func seismicFiltersControllerDidUpdateFilters(_ controller: SeismicFiltersViewController) {
model.filter = controller.currentFilterType
loadData(forced: controller.needsDataUpdate)
refreshUI()
}
}
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()
@@ -398,9 +911,12 @@ extension SeismicNetworksViewController: SeismicCardSettingsViewControllerDelega
}
extension SeismicNetworksViewController: DZNEmptyDataSetSource {
func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! {
func title(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString? {
let text = EQNSeismic.shared.filterOption == .positionRelevant
? NSLocalizedString("filter_empty_relevant", comment: "")
: NSLocalizedString("filter_empty_area", comment: "")
let attributes = [ NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .body) ]
let string = NSAttributedString(string: NSLocalizedString("filter_empty", comment: ""), attributes: attributes)
let string = NSAttributedString(string: text, attributes: attributes)
return string
}
}
@@ -1,118 +0,0 @@
//
// SeismicSettingsNetworksViewController.swift
// Earthquake Network
//
// Created by Busi Andrea on 14/09/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
import UIKit
protocol SeismicSettingsNetworksViewControllerDelegate: AnyObject {
func seismicSettingsNetworksControllerDidComplete(_ controller: SeismicSettingsNetworksViewController)
}
class SeismicSettingsNetworksViewController: UITableViewController {
weak var delegate: SeismicSettingsNetworksViewControllerDelegate?
// MARK: - Private
private var networks = [EQNSeismicNetwork]()
private var savedNetworks = [String]()
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(SettingDetailTableViewCell.self, forCellReuseIdentifier: SettingDetailTableViewCell.Identifier)
tableView.register(SettingSectionHeaderView.self, forHeaderFooterViewReuseIdentifier: SettingSectionHeaderView.Identifier)
loadData()
}
// MARK: - Private
private func loadData() {
networks = EQNData.seismicNetworks().sorted(by: { $0.acronym < $1.acronym })
// load saved selected networks or fill with all available networks
let savedNetworks = EQNUserData.shared.seismicNetworksSelected()
if !savedNetworks.isEmpty {
self.savedNetworks = savedNetworks
} else {
self.savedNetworks = EQNData.seismicNetworkAcronyms()
}
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SettingSectionHeaderView.Identifier) as! SettingSectionHeaderView
headerView.titleLabel.text = NSLocalizedString("options_agencies", comment: "");
return headerView
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
CGFloat(SettingSectionHeaderView.Height)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
networks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let network = networks[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: SettingDetailTableViewCell.Identifier, for: indexPath) as! SettingDetailTableViewCell
cell.textLabel?.text = "\(network.acronym) (\(network.country))"
if savedNetworks.contains(network.acronym) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let network = networks[indexPath.row]
if let index = savedNetworks.firstIndex(of: network.acronym) {
savedNetworks.remove(at: index)
} else {
savedNetworks.append(network.acronym)
}
// reload all rows with the given acronym
let indexes = networks
.enumerated()
.filter { $0.element.acronym == network.acronym }
.map { IndexPath(row: $0.offset, section: 0) }
tableView.reloadRows(at: indexes, with: .automatic)
}
// MARK: - Actions
@IBAction func cancelTapped(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
@IBAction func saveTapped(_ sender: Any) {
// save selected networks
EQNUserData.shared.saveSelectedSeismicNetworks(savedNetworks)
// se solo un'ente è selezionato, salviamolo anche come nazione
if savedNetworks.count == 1 {
UserDefaults.standard.set(savedNetworks.first!, forKey: IMPOSTAZIONE_NAZIONE_RETI_SISMICHE)
} else {
UserDefaults.standard.removeObject(forKey: IMPOSTAZIONE_NAZIONE_RETI_SISMICHE)
}
delegate?.seismicSettingsNetworksControllerDidComplete(self)
dismiss(animated: true, completion: nil)
}
}
@@ -1,130 +0,0 @@
//
// SeismicSettingsViewController.swift
// Earthquake Network
//
// Created by Busi Andrea on 13/09/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
import Foundation
protocol SeismicSettingsViewControllerDelegate: AnyObject {
func seismicSettingsControllerDidComplete(_ controller: SeismicSettingsViewController)
func seismicSettingsControllerWillOpenProviders(_ controller: SeismicSettingsViewController)
}
class SeismicSettingsViewController: UIViewController {
weak var delegate: SeismicSettingsViewControllerDelegate?
// MARK: - Private
@IBOutlet private weak var containerView: UIView!
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var countryTextField: UITextField!
@IBOutlet private weak var confirmButton: UIButton!
@IBOutlet private weak var otherwiseLabel: UILabel!
@IBOutlet private weak var manageNetworksButton: UIButton!
@IBOutlet private weak var cancelButton: UIButton!
private let networks = EQNData.seismicNetworks().sorted(by: { $0.country < $1.country })
private let picker = EQNGenericPickerViewController()
private var selectedNetwork: EQNSeismicNetwork?
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
// MARK: - Private
private func setupUI() {
containerView.layer.cornerRadius = AppTheme.shared.cardCornerRadius
containerView.layer.masksToBounds = true
// localize
titleLabel.text = NSLocalizedString("official_select_country", comment: "")
countryTextField.placeholder = NSLocalizedString("official_select_country_placeholder", comment: "")
confirmButton.setLocalizedTitle(key: "official_select_confirm", uppercased: false)
otherwiseLabel.text = NSLocalizedString("official_select_or", comment: "")
manageNetworksButton.setLocalizedTitle(key: "official_select_networks", uppercased: false)
cancelButton.setLocalizedTitle(key: "status_cancel", uppercased: false)
// load saved country (if exists)
let savedCountry = UserDefaults.standard.object(forKey: IMPOSTAZIONE_NAZIONE_RETI_SISMICHE) as? String
selectedNetwork = EQNData.seismic(for: savedCountry)
countryTextField.text = selectedNetwork?.country
countryTextField.inputView = picker.view
let selectedIndex: Int? = selectedNetwork != nil ? networks.firstIndex(of: selectedNetwork!) : nil
picker.configure(with: networks, selectedIndex: selectedIndex) { [unowned self] (network) in
guard let network = network as? EQNSeismicNetwork else { return }
self.view.endEditing(true)
self.selectedNetwork = network
self.countryTextField.text = self.selectedNetwork?.country
}
picker.onCancel = { [unowned self] in
self.view.endEditing(true)
}
}
private func performSave(for network: EQNSeismicNetwork) {
// salviamo la sigla dell'ente selezionato
UserDefaults.standard.set(network.acronym, forKey: IMPOSTAZIONE_NAZIONE_RETI_SISMICHE)
// gli enti selezionati conterranno solo l'ente della nazione selezionata
let selectedNetworks = [network.acronym]
EQNUserData.shared.saveSelectedSeismicNetworks(selectedNetworks)
// aggiorniamo le impostazioni di notifica
EQNNotificheReteSismiche.shared().listaEnti = selectedNetworks
EQNNotificheReteSismiche.shared().saveUserInfo()
SettingsBaseViewController.saveSettings()
// informiamo il delegato
delegate?.seismicSettingsControllerDidComplete(self)
dismiss(animated: true, completion: nil)
}
// MARK: - Actions
@IBAction func confirmCountryTapped(_ sender: UIButton) {
guard let network = selectedNetwork else {
let alert = UIAlertController(title: NSLocalizedString("attention", comment: ""),
message: NSLocalizedString("official_no_country_selected", comment: ""),
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("ok", comment: ""), style: .cancel, handler: { [unowned self] (action) in
self.countryTextField.becomeFirstResponder()
}))
present(alert, animated: true, completion: nil)
return
}
// ask confirm to change settings for notifications
let alert = UIAlertController(title: NSLocalizedString("attention", comment: ""),
message: NSLocalizedString("official_select_message", comment: ""),
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("status_cancel", comment: ""), style: .cancel))
alert.addAction(UIAlertAction(title: NSLocalizedString("official_select_confirm", comment: ""), style: .default, handler: { [unowned self] (action) in
self.performSave(for: network)
}))
present(alert, animated: true, completion: nil)
}
@IBAction func selectNetworksTapped(_ sender: UIButton) {
delegate?.seismicSettingsControllerWillOpenProviders(self)
dismiss(animated: true, completion: nil)
}
@IBAction func cancelTapped(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
}
@@ -11,27 +11,27 @@ import Foundation
class SettingDateTableViewCell: UITableViewCell {
@objc static let Identifier = "DateCell"
static let Identifier = "DateCell"
@objc var isDisabled: Bool = false {
var isDisabled: Bool = false {
didSet {
updateUI()
}
}
@objc var isPickerVisible: Bool = false {
var isPickerVisible: Bool = false {
didSet {
if oldValue != isPickerVisible {
updateUI()
}
}
}
@objc private(set) var date = Date()
@objc var valueChanged: ((Date) -> Void)?
private(set) var date = Date()
var valueChanged: ((Date) -> Void)?
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -40,7 +40,7 @@ class SettingDateTableViewCell: UITableViewCell {
return label
}()
@objc lazy var valuesLabel: UILabel = {
lazy var valuesLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -58,7 +58,7 @@ class SettingDateTableViewCell: UITableViewCell {
return picker
}()
@objc lazy var stackView: UIStackView = {
lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
@@ -81,7 +81,7 @@ class SettingDateTableViewCell: UITableViewCell {
// MARK: - Public
@objc public func updateDate(_ date: Date) {
public func updateDate(_ date: Date) {
self.date = date
datePicker.setDate(date, animated: true)
}
@@ -10,17 +10,16 @@ import UIKit
class SettingDetailTableViewCell: UITableViewCell {
@objc static let Identifier = "DetailCell"
static let Identifier = "DetailCell"
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
@@ -10,10 +10,10 @@ import UIKit
class SettingEnableTableViewCell: UITableViewCell {
@objc static let Identifier = "EnableCell"
static let Identifier = "EnableCell"
@objc var valueChanged: ((Bool) -> Void)?
@objc var isDisabled: Bool = false {
var valueChanged: ((Bool) -> Void)?
var isDisabled: Bool = false {
didSet {
updateUI()
}
@@ -21,7 +21,7 @@ class SettingEnableTableViewCell: UITableViewCell {
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -29,7 +29,7 @@ class SettingEnableTableViewCell: UITableViewCell {
return label
}()
@objc lazy var descriptionLabel: UILabel = {
lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -37,7 +37,7 @@ class SettingEnableTableViewCell: UITableViewCell {
return label
}()
@objc lazy var toggleSwitch: UISwitch = {
lazy var toggleSwitch: UISwitch = {
let toggle = UISwitch()
toggle.setContentHuggingPriority(.required, for: .horizontal)
toggle.setContentCompressionResistancePriority(.required, for: .horizontal)
@@ -45,6 +45,15 @@ class SettingEnableTableViewCell: UITableViewCell {
return toggle
}()
lazy var errorLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = UIFont.preferredFont(forTextStyle: .subheadline)
label.textColor = AppTheme.Colors.red
label.text = nil
return label
}()
// MARK: - Init
@@ -75,6 +84,7 @@ class SettingEnableTableViewCell: UITableViewCell {
contentView.addSubview(stackView)
contentView.addSubview(descriptionLabel)
contentView.addSubview(errorLabel)
stackView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor).isActive = true
@@ -83,7 +93,12 @@ class SettingEnableTableViewCell: UITableViewCell {
descriptionLabel.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 8).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
descriptionLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
//descriptionLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
errorLabel.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 8.0).isActive = true
errorLabel.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
errorLabel.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor).isActive = true
errorLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
}
private func updateUI() {
@@ -11,9 +11,9 @@ import Foundation
class SettingMultivaluesTableViewCell: UITableViewCell {
@objc static let Identifier = "MultivaluesCell"
static let Identifier = "MultivaluesCell"
@objc var isDisabled: Bool = false {
var isDisabled: Bool = false {
didSet {
updateUI()
}
@@ -21,7 +21,7 @@ class SettingMultivaluesTableViewCell: UITableViewCell {
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -30,7 +30,7 @@ class SettingMultivaluesTableViewCell: UITableViewCell {
return label
}()
@objc lazy var valuesLabel: UILabel = {
lazy var valuesLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -10,18 +10,17 @@ import UIKit
class SettingSectionHeaderView: UITableViewHeaderFooterView {
@objc static let Identifier = "SectionHeaderView"
@objc static let Height = 50.0
static let Identifier = "SectionHeaderView"
static let Height = 50.0
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let titleLabel = UILabel()
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
titleLabel.textColor = AppTheme.Colors.lightBlue
return titleLabel
}()
// MARK: - Init
@@ -34,7 +33,6 @@ class SettingSectionHeaderView: UITableViewHeaderFooterView {
super.init(coder: coder)
setupUI()
}
// MARK: - Private
@@ -45,6 +43,5 @@ class SettingSectionHeaderView: UITableViewHeaderFooterView {
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
}
}
@@ -11,19 +11,19 @@ import Foundation
class SettingSegmentedTableViewCell: UITableViewCell {
@objc static let Identifier = "SegmentedCell"
static let Identifier = "SegmentedCell"
@objc var isDisabled: Bool = false {
var isDisabled: Bool = false {
didSet {
updateUI()
}
}
@objc var valueChanged: ((EQNGenericValue) -> Void)?
var valueChanged: ((EQNGenericValue) -> Void)?
private var items = [EQNGenericValue]()
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -32,7 +32,7 @@ class SettingSegmentedTableViewCell: UITableViewCell {
return label
}()
@objc lazy var segmentedControl: UISegmentedControl = {
lazy var segmentedControl: UISegmentedControl = {
let segmented = UISegmentedControl()
segmented.translatesAutoresizingMaskIntoConstraints = false
segmented.addTarget(self, action: #selector(segmentedControlChanged(_:)), for: .valueChanged)
@@ -10,21 +10,21 @@ import UIKit
class SettingSliderTableViewCell: UITableViewCell {
@objc static let Identifier = "SliderCell"
static let Identifier = "SliderCell"
@objc var isDisabled: Bool = false {
var isDisabled: Bool = false {
didSet {
updateUI()
}
}
@objc var valueChanged: ((EQNGenericValue) -> Void)?
@objc var dragEnded: (() -> Void)?
var valueChanged: ((EQNGenericValue) -> Void)?
var dragEnded: (() -> Void)?
private var items = [EQNGenericValue]()
// MARK: - Properties
@objc lazy var titleLabel: UILabel = {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -33,7 +33,7 @@ class SettingSliderTableViewCell: UITableViewCell {
return label
}()
@objc lazy var valueLabel: UILabel = {
lazy var valueLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
@@ -42,7 +42,7 @@ class SettingSliderTableViewCell: UITableViewCell {
return label
}()
@objc lazy var slider: UISlider = {
lazy var slider: UISlider = {
let slider = UISlider()
slider.isContinuous = true
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
@@ -0,0 +1,43 @@
//
// SettingsBaseTableViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 10/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import UIKit
@objc
class SettingsBaseTableViewController: UITableViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParent {
Self.saveSettings()
}
}
// MARK: - Class
@objc class func saveSettings() {
saveSettings { _ in
// nope
}
}
@objc class func saveSettings(
completion: @escaping (_ success: Bool) -> Void
) {
let url = EQNGeneratoreURLServer.urlInvioImpostazioniNotifiche()
ServerRequest.default().inviaInformazioniAlServer(with: url, richiesta: .impostazioniNotifiche) { _ in
print("[SETTINGS] Settings saved successfully")
completion(true)
} failure: { error in
print("[SETTINGS] Settings saved failed. Error: \(error?.localizedDescription ?? "n.d.")")
completion(false)
}
}
}
@@ -1,19 +0,0 @@
//
// SettingsBaseViewController.h
// Earthquake Network
//
// Created by Busi Andrea on 30/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface SettingsBaseViewController : UITableViewController
+ (void)saveSettings;
@end
NS_ASSUME_NONNULL_END
@@ -1,42 +0,0 @@
//
// SettingsBaseViewController.m
// Earthquake Network
//
// Created by Busi Andrea on 30/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "SettingsBaseViewController.h"
#import "ServerRequest.h"
#import "EQNGeneratoreURLServer.h"
@interface SettingsBaseViewController ()
@end
@implementation SettingsBaseViewController
#pragma mark - View Lifecycle
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// when controller is dismissed, save settings
if (self.isMovingFromParentViewController) {
[SettingsBaseViewController saveSettings];
}
}
#pragma mark - Private
+ (void)saveSettings
{
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:[EQNGeneratoreURLServer urlInvioImpostazioniNotifiche] richiesta:EQNTipoChiamataImpostazioniNotifiche success:^(id result){
NSLog(@"Settings saved successfully");
} failure:^(NSError *error){
NSLog(@"Settings saved failed. Error: %@", error.localizedDescription);
}];
}
@end
@@ -1,18 +0,0 @@
//
// AletaSismiTableViewController.h
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "SettingsBaseViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface SettingsRealTimeAlertsViewController : SettingsBaseViewController
@end
NS_ASSUME_NONNULL_END
@@ -1,165 +0,0 @@
//
// AletaSismiTableViewController.m
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "SettingsRealTimeAlertsViewController.h"
#import "EQNAllertaSismica.h"
@import UserNotifications;
@interface SettingsRealTimeAlertsViewController () <UITextFieldDelegate>
@property (nonatomic, strong) NSArray<SettingItem *> *settings;
@property (nonatomic, strong) NSDateFormatter *dateFormatter;
@property (nonatomic, assign) BOOL notificationEnabled;
@property (nonatomic, assign) BOOL criticalAlertsEnabled;
@end
@implementation SettingsRealTimeAlertsViewController
typedef NS_ENUM(NSInteger, RowIdentifier) {
RowIdentifierAbilitaNotifiche = 0,
RowIdentifierAbilitaCriticalAlerts
};
#pragma mark - Accessories
- (NSDateFormatter *)dateFormatter
{
if (!_dateFormatter) {
_dateFormatter = [[NSDateFormatter alloc] init];
[_dateFormatter setDateFormat:@"HH:mm"];
}
return _dateFormatter;
}
#pragma mark - View Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self setupUI];
self.settings = @[
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"options_notification_enable_alarm", @"")],
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"critical_alerts_setting", @"")]
];
[self loadDataSource];
[self.tableView reloadData];
}
#pragma mark - Private
- (void)setupUI
{
self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever;
self.tableView.estimatedRowHeight = 200.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.tableView registerClass:[SettingSectionHeaderView class] forHeaderFooterViewReuseIdentifier:SettingSectionHeaderView.Identifier];
[self.tableView registerClass:[SettingEnableTableViewCell class] forCellReuseIdentifier:SettingEnableTableViewCell.Identifier];
[self.tableView registerClass:[SettingSliderTableViewCell class] forCellReuseIdentifier:SettingSliderTableViewCell.Identifier];
[self.tableView registerClass:[SettingMultivaluesTableViewCell class] forCellReuseIdentifier:SettingMultivaluesTableViewCell.Identifier];
[self.tableView registerClass:[SettingSegmentedTableViewCell class] forCellReuseIdentifier:SettingSegmentedTableViewCell.Identifier];
[self.tableView registerClass:[SettingDateTableViewCell class] forCellReuseIdentifier:SettingDateTableViewCell.Identifier];
}
- (void)loadDataSource
{
self.notificationEnabled = [EQNAllertaSismica sharedInstance].isAbilitato;
self.criticalAlertsEnabled = [EQNAllertaSismica sharedInstance].isCriticalAlertsEnabled;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.settings.count;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
SettingSectionHeaderView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:SettingSectionHeaderView.Identifier];
headerView.titleLabel.text = NSLocalizedString(@"options_alarms", @"");
return headerView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return SettingSectionHeaderView.Height;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
SettingItem *setting = self.settings[indexPath.row];
if (setting.type == SettingTypeEnable) {
SettingEnableTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingEnableTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
cell.descriptionLabel.text = setting.subtitle;
if (indexPath.row == RowIdentifierAbilitaNotifiche) {
cell.toggleSwitch.on = self.notificationEnabled;
cell.valueChanged = ^(BOOL enabled) {
self.notificationEnabled = enabled;
[EQNAllertaSismica sharedInstance].isAbilitato = self.notificationEnabled;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
} else if (indexPath.row == RowIdentifierAbilitaCriticalAlerts) {
cell.toggleSwitch.on = self.criticalAlertsEnabled;
cell.valueChanged = ^(BOOL enabled) {
if (enabled) {
[self askForCriticalAlertsPermission];
}
self.criticalAlertsEnabled = enabled;
[EQNAllertaSismica sharedInstance].isCriticalAlertsEnabled = self.criticalAlertsEnabled;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
}
return cell;
} else if (setting.type == SettingTypeSegmented) {
SettingSegmentedTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingSegmentedTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
return cell;
} else if (setting.type == SettingTypeSlider) {
SettingSliderTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingSliderTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
return cell;
}
return nil;
}
#pragma mark - Private
- (void)updateSismicToNotify:(EQNGenericValue *)seismic
{
[EQNAllertaSismica sharedInstance].sismiDaNotificare = seismic.value;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self loadDataSource];
}
- (void)askForCriticalAlertsPermission
{
UNAuthorizationOptions authOptions = UNAuthorizationOptionCriticalAlert;
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError *error) {
// nope
}];
}
@end
@@ -0,0 +1,153 @@
//
// 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 disabilitaSuonoAllerta
case abilitaCriticalAlerts
}
private var isNotificationEnabled = false
private var isMildQuakeSoundDisabled = false
private var isCriticalAlertsEnabled = false
private let settings: [SettingItem] = [
.init(type: .enable, title: NSLocalizedString("options_notification_enable_alarm", comment: "")),
.init(type: .enable, title: NSLocalizedString("options_notification_disable_sound", comment: "")),
.init(type: .enable, title: NSLocalizedString("critical_alerts_setting", comment: ""))
]
// 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
isMildQuakeSoundDisabled = saved.isMildQuakeSoundDisabled
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 .disabilitaSuonoAllerta:
cell.toggleSwitch.isOn = isMildQuakeSoundDisabled
cell.valueChanged = { [weak self] enabled in
self?.onChangeDisableSoundEnabled(enabled)
}
case .abilitaCriticalAlerts:
cell.toggleSwitch.isOn = isCriticalAlertsEnabled
cell.valueChanged = { [weak self] enabled in
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 onChangeDisableSoundEnabled(_ enabled: Bool) {
isMildQuakeSoundDisabled = enabled
EQNSettingRealTimeAlert.shared.isMildQuakeSoundDisabled = isMildQuakeSoundDisabled
EQNSettingRealTimeAlert.shared.saveUserInfo()
tableView.reloadData()
}
private func onChangeCriticalAlertsEnabled(_ enabled: Bool) {
if enabled {
askForCriticalAlertsPermission()
}
isCriticalAlertsEnabled = enabled
EQNSettingRealTimeAlert.shared.isCriticalAlertsEnabled = isCriticalAlertsEnabled
EQNSettingRealTimeAlert.shared.saveUserInfo()
tableView.reloadData()
}
private func askForCriticalAlertsPermission() {
UNUserNotificationCenter.current().requestAuthorization(options: [ .criticalAlert ]) { granted, error in
// nope
}
}
}
@@ -1,18 +0,0 @@
//
// SettingsSeismicNetworkAlertsViewController.h
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "SettingsBaseViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface SettingsSeismicNetworkAlertsViewController : SettingsBaseViewController
@end
NS_ASSUME_NONNULL_END
@@ -1,258 +0,0 @@
//
// SettingsSeismicNetworkAlertsViewController.m
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "SettingsSeismicNetworkAlertsViewController.h"
#import "EQNNotificheReteSismiche.h"
@interface SettingsSeismicNetworkAlertsViewController ()
@property (nonatomic, strong) NSArray<SettingItem *> *settings;
@property (nonatomic, strong) NSArray<EQNGenericValue *> *dataSourceRaggioSisma;
@property (nonatomic, strong) NSArray<EQNGenericValue *> *dataSourceMagnitudoDeboli;
@property (nonatomic, strong) NSArray<EQNGenericValue *> *dataSourceMagnitudoForti;
@property (nonatomic, assign) BOOL notificationEnabled;
@property (nonatomic, assign) BOOL notificationNearEarthquakeEnabled;
@property (nonatomic, assign) BOOL notificationStrongEarthquakeEnabled;
@property (nonatomic, strong) EQNGenericValue *currentUserPositionRadius;
@property (nonatomic, strong) EQNGenericValue *currentSeismicEnergy;
@property (nonatomic, strong) EQNGenericValue *currentStrongEarthquakeDistance;
@end
@implementation SettingsSeismicNetworkAlertsViewController
static NSString * const SegueIdentifierListaEnti = @"ShowListaEnti";
typedef NS_ENUM(NSInteger, RowIdentifier) {
RowIdentifierAbilitaNotifiche = 0,
RowIdentifierRetiSismiche,
RowIdentifierRaggioPosizione,
RowIdentifierEnergiaSisma,
RowIdentifierTerremotiVicini,
RowIdentifierTerremotiForti,
RowIdentifierTerremotiFortiDistanza
};
#pragma mark - View Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self setupUI];
self.settings = @[
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"options_notification_enable_official", @"")],
[[SettingItem alloc] initWithType:SettingTypeMultiValues title:NSLocalizedString(@"options_agencies", @"") segue:SegueIdentifierListaEnti],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_radius", @"")],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_energy", @"")],
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"options_near", @"") subtitle:NSLocalizedString(@"options_near_alert", @"")],
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"options_strong", @"") subtitle:NSLocalizedString(@"options_strong_alert", @"")],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_strong_magnitude", @"")]
];
self.dataSourceMagnitudoDeboli = [EQNData magitudoDeboli];
self.dataSourceRaggioSisma = [EQNData raggioSismi];
self.dataSourceMagnitudoForti = [EQNData magitudoForti];
[self loadDataSource];
[self.tableView reloadData];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self loadDataSource];
[self.tableView reloadData];
}
#pragma mark - Private
- (void)setupUI
{
self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever;
self.tableView.estimatedRowHeight = 200.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.tableView registerClass:[SettingSectionHeaderView class] forHeaderFooterViewReuseIdentifier:SettingSectionHeaderView.Identifier];
[self.tableView registerClass:[SettingEnableTableViewCell class] forCellReuseIdentifier:SettingEnableTableViewCell.Identifier];
[self.tableView registerClass:[SettingSliderTableViewCell class] forCellReuseIdentifier:SettingSliderTableViewCell.Identifier];
[self.tableView registerClass:[SettingMultivaluesTableViewCell class] forCellReuseIdentifier:SettingMultivaluesTableViewCell.Identifier];
}
- (void)loadDataSource
{
self.notificationEnabled = [EQNNotificheReteSismiche sharedInstance].isAbilitato;
self.notificationNearEarthquakeEnabled = [EQNNotificheReteSismiche sharedInstance].isAbilitaVicini;
self.notificationStrongEarthquakeEnabled = [EQNNotificheReteSismiche sharedInstance].isTerremortiForti;
// raggio dalla tua posizione
EQNGenericValue *raggioSisma = [EQNData raggioSismaFor:[EQNNotificheReteSismiche sharedInstance].distanzaPosizione];
self.currentUserPositionRadius = raggioSisma;
// energia sisma
EQNGenericValue *energiaSisma = [EQNData magitudoDeboleFor:[EQNNotificheReteSismiche sharedInstance].energiaSisma];
self.currentSeismicEnergy = energiaSisma;
// terremoti forti
EQNGenericValue *terremotiForti = [EQNData magitudoForteFor:[EQNNotificheReteSismiche sharedInstance].energiaTerremotiForti];
self.currentStrongEarthquakeDistance = terremotiForti;
// enti
if (![EQNNotificheReteSismiche sharedInstance].listaEnti) {
[EQNNotificheReteSismiche sharedInstance].listaEnti = [EQNData.seismicNetworkAcronyms copy];
}
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
}
#pragma mark - Table view data source
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
SettingSectionHeaderView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:SettingSectionHeaderView.Identifier];
headerView.titleLabel.text = NSLocalizedString(@"options_notification_official", @"titolo impostazioni notifiche");
return headerView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return SettingSectionHeaderView.Height;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.settings.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
SettingItem *setting = self.settings[indexPath.row];
if (setting.type == SettingTypeEnable) {
SettingEnableTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingEnableTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
cell.descriptionLabel.text = setting.subtitle;
if (indexPath.row == RowIdentifierAbilitaNotifiche) {
cell.toggleSwitch.on = self.notificationEnabled;
cell.valueChanged = ^(BOOL enabled) {
self.notificationEnabled = enabled;
[EQNNotificheReteSismiche sharedInstance].isAbilitato = self.notificationEnabled;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
} else if (indexPath.row == RowIdentifierTerremotiVicini) {
cell.toggleSwitch.on = self.notificationNearEarthquakeEnabled;
cell.isDisabled = !self.notificationEnabled;
cell.valueChanged = ^(BOOL enabled) {
self.notificationNearEarthquakeEnabled = enabled;
[EQNNotificheReteSismiche sharedInstance].isAbilitaVicini = self.notificationNearEarthquakeEnabled;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
} else if (indexPath.row == RowIdentifierTerremotiForti) {
cell.toggleSwitch.on = self.notificationStrongEarthquakeEnabled;
cell.isDisabled = !self.notificationEnabled;
cell.valueChanged = ^(BOOL enabled) {
self.notificationStrongEarthquakeEnabled = enabled;
[EQNNotificheReteSismiche sharedInstance].isTerremortiForti = self.notificationStrongEarthquakeEnabled;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
}
return cell;
} else if (setting.type == SettingTypeMultiValues) {
SettingMultivaluesTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingMultivaluesTableViewCell.Identifier forIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.isDisabled = !self.notificationEnabled;
cell.userInteractionEnabled = self.notificationEnabled;
cell.titleLabel.text = setting.title;
if (indexPath.row == RowIdentifierRetiSismiche) {
cell.valuesLabel.text = [self stringOfSelectedNetworks];
}
return cell;
} else if (setting.type == SettingTypeSlider) {
SettingSliderTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingSliderTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
if (indexPath.row == RowIdentifierRaggioPosizione) {
cell.isDisabled = !self.notificationEnabled;
[cell configureSliderWith:self.dataSourceRaggioSisma current:self.currentUserPositionRadius];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateUserPositionRadius:item];
};
} else if (indexPath.row == RowIdentifierEnergiaSisma) {
cell.isDisabled = !self.notificationEnabled;
[cell configureSliderWith:self.dataSourceMagnitudoDeboli current:self.currentSeismicEnergy];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateSeismicEnergy:item];
};
} else if (indexPath.row == RowIdentifierTerremotiFortiDistanza) {
cell.isDisabled = !self.notificationEnabled || !self.notificationStrongEarthquakeEnabled;
[cell configureSliderWith:self.dataSourceMagnitudoForti current:self.currentStrongEarthquakeDistance];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateStrongEarthquakeEnergy:item];
};
}
return cell;
}
return nil;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
SettingItem *setting = self.settings[indexPath.row];
if (setting.segue != nil) {
[self performSegueWithIdentifier:setting.segue sender:nil];
}
}
#pragma mark - Private
- (void)updateUserPositionRadius:(EQNGenericValue *)radius
{
[EQNNotificheReteSismiche sharedInstance].distanzaPosizione = radius.value;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self loadDataSource];
}
- (void)updateSeismicEnergy:(EQNGenericValue *)energy
{
[EQNNotificheReteSismiche sharedInstance].energiaSisma = energy.value;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self loadDataSource];
}
- (void)updateStrongEarthquakeEnergy:(EQNGenericValue *)energy
{
[EQNNotificheReteSismiche sharedInstance].energiaTerremotiForti = energy.value;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self loadDataSource];
}
- (NSString *)stringOfSelectedNetworks
{
NSArray *networks = [EQNNotificheReteSismiche sharedInstance].listaEnti;
networks = [networks sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
return [networks componentsJoinedByString:@", "];
}
@end
@@ -0,0 +1,166 @@
//
// SettingsSeismicNetworkNotificationsViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 06/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import UIKit
import Shogun
class SettingsSeismicNetworkNotificationsViewController: SettingsBaseTableViewController {
private enum RowIdentifier: Int {
case abilitaNotifiche
case magnitudoMinima
case distanzaMassima
}
private var isNotificationEnabled = false
private var currentMinimumMagnitude = EQNData.DefaultSettingSeismicNetworkNotificationMagitude
private var currentMaximumDistance = EQNData.DefaultSettingSeismicNetworkNotificationRadius
private let dataSourceMinimumMagnitude = EQNData.settingSeismicNetworkNotificationMagnitudes
private let dataSourceMaximumDistance = EQNData.settingSeismicNetworkNotificationRadius
private let settings: [SettingItem] = [
.init(type: .enable, title: NSLocalizedString("options_notification_enable_official", comment: "")),
.init(type: .slider, title: NSLocalizedString("options_official_minmag", comment: "")),
.init(type: .slider, title: NSLocalizedString("options_official_maxdist", comment: ""))
]
// MARK: - View Liefcycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
tableView.reloadData()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadDataSource()
tableView.reloadData()
}
// MARK: - Private
private func setupUI() {
navigationItem.largeTitleDisplayMode = .never
tableView.estimatedRowHeight = 200.0
tableView.rowHeight = UITableView.automaticDimension
tableView.registerHeaderFooterView(for: SettingSectionHeaderView.self)
tableView.registerCell(for: SettingEnableTableViewCell.self)
tableView.registerCell(for: SettingSliderTableViewCell.self)
tableView.registerCell(for: SettingMultivaluesTableViewCell.self)
}
private func loadDataSource() {
let saved = EQNSettingSeismicNetworkNotification.shared
isNotificationEnabled = saved.isAbilitato
// magnitudo minima
let magnitudoMinima = EQNData.getSettingSeismicNetworkNotificationMagnitudes(for: saved.magnitudoMinima)
currentMinimumMagnitude = magnitudoMinima
// raggio dalla tua posizione
let distanzaMassima = EQNData.getSettingSeismicNetworkNotificationRadius(for: saved.distanzaMassima)
currentMaximumDistance = distanzaMassima
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueHeaderFooterView(cellIdentifiable: SettingSectionHeaderView.self)
view.titleLabel.text = NSLocalizedString("options_notification_official", comment: "")
return view
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
SettingSectionHeaderView.Height
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return settings.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let identifier = RowIdentifier(rawValue: indexPath.row) else {
return UITableViewCell()
}
let setting = settings[indexPath.row]
switch setting.type {
case .enable:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SettingEnableTableViewCell.self, for: indexPath)
cell.titleLabel.text = setting.displayTitle
cell.descriptionLabel.text = setting.subtitle
switch identifier {
case .abilitaNotifiche:
cell.toggleSwitch.isOn = isNotificationEnabled
cell.valueChanged = { [weak self] enabled in
self?.onChangeNotificationEnabled(enabled)
}
default:
break
}
return cell
case .slider:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SettingSliderTableViewCell.self, for: indexPath)
cell.titleLabel.text = setting.displayTitle
let filtersEnabled = isNotificationEnabled
switch identifier {
case .magnitudoMinima:
cell.isDisabled = !filtersEnabled
cell.configureSlider(with: dataSourceMinimumMagnitude, current: currentMinimumMagnitude)
cell.valueChanged = { [weak self] item in
self?.onChangeMinimumMagnitude(item)
}
case .distanzaMassima:
cell.isDisabled = !filtersEnabled
cell.configureSlider(with: dataSourceMaximumDistance, current: currentMaximumDistance)
cell.valueChanged = { [weak self] item in
self?.onChangeMaximumDistance(item)
}
default:
break
}
return cell
default:
fatalError()
}
}
private func onChangeNotificationEnabled(_ enabled: Bool) {
isNotificationEnabled = enabled
EQNSettingSeismicNetworkNotification.shared.isAbilitato = isNotificationEnabled
EQNSettingSeismicNetworkNotification.shared.saveUserInfo()
tableView.reloadData()
}
private func onChangeMinimumMagnitude(_ item: EQNGenericValue) {
EQNSettingSeismicNetworkNotification.shared.magnitudoMinima = item.value
EQNSettingSeismicNetworkNotification.shared.saveUserInfo()
loadDataSource()
}
private func onChangeMaximumDistance(_ item: EQNGenericValue) {
EQNSettingSeismicNetworkNotification.shared.distanzaMassima = item.value
EQNSettingSeismicNetworkNotification.shared.saveUserInfo()
loadDataSource()
}
}
@@ -1,71 +0,0 @@
//
// SettingsSeismicNetworksViewController.swift
// Earthquake Network
//
// Created by Busi Andrea on 26/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
import UIKit
class SettingsSeismicNetworksViewController: UITableViewController {
var networks = [EQNSeismicNetwork]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(SettingDetailTableViewCell.self, forCellReuseIdentifier: SettingDetailTableViewCell.Identifier)
tableView.register(SettingSectionHeaderView.self, forHeaderFooterViewReuseIdentifier: SettingSectionHeaderView.Identifier)
networks = EQNData.seismicNetworks().sorted(by: { $0.acronym < $1.acronym })
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SettingSectionHeaderView.Identifier) as! SettingSectionHeaderView
headerView.titleLabel.text = NSLocalizedString("options_agencies", comment: "");
return headerView
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
CGFloat(SettingSectionHeaderView.Height)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
networks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let network = networks[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: SettingDetailTableViewCell.Identifier, for: indexPath) as! SettingDetailTableViewCell
cell.textLabel?.text = "\(network.acronym) (\(network.country))"
if EQNNotificheReteSismiche.shared().listaEnti.contains(network.acronym) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let network = networks[indexPath.row]
var savedNetworks = EQNNotificheReteSismiche.shared().listaEnti
if let index = savedNetworks.firstIndex(where: { $0 == network.acronym }) {
savedNetworks.remove(at: index)
} else {
savedNetworks.append(network.acronym)
}
EQNNotificheReteSismiche.shared().listaEnti = savedNetworks
EQNNotificheReteSismiche.shared().saveUserInfo()
tableView.reloadData()
}
}
@@ -1,18 +0,0 @@
//
// SettingsUserReportAlertsViewController.h
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "SettingsBaseViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface SettingsUserReportAlertsViewController : SettingsBaseViewController
@end
NS_ASSUME_NONNULL_END
@@ -1,119 +0,0 @@
//
// SettingsUserReportAlertsViewController.m
// Earthquake Network
//
// Refactored by Andrea Busi 25/08/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "SettingsUserReportAlertsViewController.h"
#import "EQNNotificheSegnalazioniUtente.h"
@interface SettingsUserReportAlertsViewController ()
@property (nonatomic, strong) NSArray<SettingItem *> *settings;
@property (nonatomic, assign) BOOL notificationsEnabled;
@property (nonatomic, strong) EQNGenericValue *currentRadius;
@end
@implementation SettingsUserReportAlertsViewController
#pragma mark - View Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self setupUI];
self.settings = @[
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"options_notification_enable_manual", @"")],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_radius", @"")]
];
[self updateUI];
}
#pragma mark - Private
- (void)setupUI
{
self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever;
self.tableView.estimatedRowHeight = 100.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.tableView registerClass:[SettingSectionHeaderView class] forHeaderFooterViewReuseIdentifier:SettingSectionHeaderView.Identifier];
[self.tableView registerClass:[SettingEnableTableViewCell class] forCellReuseIdentifier:SettingEnableTableViewCell.Identifier];
[self.tableView registerClass:[SettingSliderTableViewCell class] forCellReuseIdentifier:SettingSliderTableViewCell.Identifier];
}
- (void)updateUI
{
self.notificationsEnabled = [EQNNotificheSegnalazioniUtente sharedInstance].isAbilitato;
EQNGenericValue *distanzaPosizione = [EQNData raggioSismaFor:[EQNNotificheSegnalazioniUtente sharedInstance].distanzaPosizione];
self.currentRadius = distanzaPosizione;
[[EQNNotificheSegnalazioniUtente sharedInstance] saveUserInfo];
[self.tableView reloadData];
}
- (void)updateRadius:(EQNGenericValue *)radius
{
self.currentRadius = radius;
[EQNNotificheSegnalazioniUtente sharedInstance].distanzaPosizione = radius.value;
[[EQNNotificheSegnalazioniUtente sharedInstance] saveUserInfo];
[self.tableView reloadData];
}
#pragma mark - Table view data source
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
SettingSectionHeaderView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:SettingSectionHeaderView.Identifier];
headerView.titleLabel.text = NSLocalizedString(@"options_notification_manual", @"titolo impostazioni notifiche");
return headerView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return SettingSectionHeaderView.Height;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.settings.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
SettingItem *setting = self.settings[indexPath.row];
if (setting.type == SettingTypeEnable) {
SettingEnableTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingEnableTableViewCell.Identifier forIndexPath:indexPath];
cell.toggleSwitch.on = self.notificationsEnabled;
cell.titleLabel.text = setting.displayTitle;
cell.descriptionLabel.text = setting.subtitle;
cell.valueChanged = ^(BOOL enabled) {
self.notificationsEnabled = enabled;
[EQNNotificheSegnalazioniUtente sharedInstance].isAbilitato = self.notificationsEnabled;
[[EQNNotificheSegnalazioniUtente sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
return cell;
} else if (setting.type == SettingTypeSlider) {
SettingSliderTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingSliderTableViewCell.Identifier forIndexPath:indexPath];
cell.isDisabled = !self.notificationsEnabled;
cell.titleLabel.text = setting.displayTitle;
[cell configureSliderWith:[EQNData raggioSismi] current:self.currentRadius];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateRadius:item];
};
return cell;
}
return nil;
}
@end
@@ -0,0 +1,145 @@
//
// SettingsUserReportNotificationsViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 10/06/24.
// Copyright © 2024 Earthquake Network. All rights reserved.
//
import UIKit
import Shogun
class SettingsUserReportNotificationsViewController: SettingsBaseTableViewController {
private enum RowIdentifier: Int {
case abilitaNotifiche
case distanzaMassima
}
private var isNotificationEnabled = false
private var currentMaximumDistance = EQNData.DefaultSettingUserReportNotificationRadius
private let dataSourceMaximumDistance = EQNData.settingUserReportNotificationRadius
private let settings: [SettingItem] = [
.init(type: .enable, title: NSLocalizedString("options_notification_enable_manual", comment: "")),
.init(type: .slider, title: NSLocalizedString("options_radius", comment: ""))
]
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
tableView.reloadData()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadDataSource()
tableView.reloadData()
}
// MARK: - Private
private func setupUI() {
navigationItem.largeTitleDisplayMode = .never
tableView.estimatedRowHeight = 200.0
tableView.rowHeight = UITableView.automaticDimension
tableView.registerHeaderFooterView(for: SettingSectionHeaderView.self)
tableView.registerCell(for: SettingEnableTableViewCell.self)
tableView.registerCell(for: SettingSliderTableViewCell.self)
}
private func loadDataSource() {
let saved = EQNSettingUserReportNotification.shared
isNotificationEnabled = saved.isAbilitato
let distanzaMassima = EQNData.getSettingUserReportNotificationRadius(for: saved.distanzaMassima)
currentMaximumDistance = distanzaMassima
}
// MARK: - Table view delegate and data source
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueHeaderFooterView(cellIdentifiable: SettingSectionHeaderView.self)
view.titleLabel.text = NSLocalizedString("options_notification_manual", comment: "")
return view
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
SettingSectionHeaderView.Height
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return settings.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let identifier = RowIdentifier(rawValue: indexPath.row) else {
return UITableViewCell()
}
let setting = settings[indexPath.row]
switch setting.type {
case .enable:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SettingEnableTableViewCell.self, for: indexPath)
cell.titleLabel.text = setting.displayTitle
cell.descriptionLabel.text = setting.subtitle
switch identifier {
case .abilitaNotifiche:
cell.toggleSwitch.isOn = isNotificationEnabled
cell.valueChanged = { [weak self] enabled in
self?.onChangeNotificationEnabled(enabled)
}
default:
break
}
return cell
case .slider:
let cell = tableView.dequeueReusableCell(cellIdentifiable: SettingSliderTableViewCell.self, for: indexPath)
cell.titleLabel.text = setting.displayTitle
switch identifier {
case .distanzaMassima:
cell.isDisabled = !isNotificationEnabled
cell.configureSlider(with: dataSourceMaximumDistance, current: currentMaximumDistance)
cell.valueChanged = { [weak self] item in
self?.onChangeMaximumDistance(item)
}
default:
break
}
return cell
default:
fatalError()
}
}
// MARK: - Private
private func onChangeNotificationEnabled(_ enabled: Bool) {
isNotificationEnabled = enabled
EQNSettingUserReportNotification.shared.isAbilitato = isNotificationEnabled
EQNSettingUserReportNotification.shared.saveUserInfo()
tableView.reloadData()
}
private func onChangeMaximumDistance(_ item: EQNGenericValue) {
EQNSettingUserReportNotification.shared.distanzaMassima = item.value
EQNSettingUserReportNotification.shared.saveUserInfo()
loadDataSource()
}
}
@@ -35,7 +35,7 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
// MARK: - Internal
/// Annotations displayed on the map
private var mapAnnotations = [MKAnnotation]()
private(set) var mapAnnotations = [MKAnnotation]()
/// If `true`, the initial filter has been already evaluated
private var initialFilterEvaluated = false
@@ -104,6 +104,34 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
return label
}()
// app icon and name displayed on the screenshot
private lazy var watermarkView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
let logo = UIImageView(image: .init(named: "eq_icon_transparent"))
logo.translatesAutoresizingMaskIntoConstraints = false
logo.contentMode = .scaleAspectFit
view.addSubview(logo)
logo.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
logo.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
logo.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
logo.widthAnchor.constraint(equalTo: logo.heightAnchor).isActive = true
let title = UILabel()
title.translatesAutoresizingMaskIntoConstraints = false
title.text = NSLocalizedString("app_name", comment: "") + " App"
title.textColor = AppTheme.Colors.red
title.font = .preferredFont(forTextStyle: .title3, weight: .semibold)
view.addSubview(title)
title.leadingAnchor.constraint(equalTo: logo.trailingAnchor, constant: 10.0).isActive = true
title.centerYAnchor.constraint(equalTo: logo.centerYAnchor).isActive = true
title.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
return view
}()
// MARK: - Init
init() {
@@ -155,6 +183,18 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
extraUI()
}
private func addWatermarkView() {
view.addSubview(watermarkView)
watermarkView.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 10.0).isActive = true
watermarkView.leadingAnchor.constraint(equalTo: mapView.leadingAnchor, constant: 10.0).isActive = true
watermarkView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
}
private func removeWatermarkView() {
watermarkView.removeFromSuperview()
}
// MARK: - View Lifecycle
override func viewDidLoad() {
@@ -238,6 +278,12 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
elaborateMapCenter()
}
func reloadMap() {
// remove and re-add annotations, to force redrawn
mapView.removeAnnotations(mapAnnotations)
mapView.addAnnotations(mapAnnotations)
}
/// Changes the center coordinate of the map to a given location
func setMapCenter(for location: CLLocation, span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 8, longitudeDelta: 8)) {
let region = MKCoordinateRegion(center: location.coordinate, span: span)
@@ -249,6 +295,31 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
// nope, subclass will implement logic
}
func zPriority(for annotation: MKAnnotation) -> MKAnnotationViewZPriority {
// subclass will impelement its own logic to define annotation priority
.min
}
func createSnapshot(
prepare: () -> Void = { },
restore: () -> Void = { }
) -> UIImage {
prepare()
addWatermarkView()
// riduciamo la porzione da salvare alla sola mappa (eliminiamo i filtri)
let size = CGSize(width: view.bounds.width, height: mapView.bounds.maxY)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { ctx in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
restore()
removeWatermarkView()
return image
}
// MARK: - Private
private func updateUI() {
@@ -286,6 +357,7 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
}
let annotationView = setupAnnotationView(for: annotation, on: mapView)
annotationView?.zPriority = zPriority(for: annotation)
return annotationView
}
@@ -244,7 +244,7 @@ class AlertSimulatorViewController: UIViewController, MKMapViewDelegate {
}
private func navigateToSubscriptions() {
let controller = SubscriptionsViewController.makeViewController()
let controller = SubscriptionsViewController()
let navigationController = UINavigationController(rootViewController: controller)
present(navigationController, animated: true)
}
+1 -15
View File
@@ -49,7 +49,7 @@ static NSString * const EQNServerUrlCalibration = @"https://srv.earthquakenetwor
// download rete smartphone
static NSString * const EQNServerUrlDownloadSmartphoneNetwork = @"https://cache.earthquakenetwork.it/distquake_count_redis3.php";
// download area check
static NSString * const EQNServerUrlDownloadAreaCheck = @"https://srv.earthquakenetwork.it/distquake_download_areacheck.php";
static NSString * const EQNServerUrlDownloadAreaCheck = @"https://cache.earthquakenetwork.it/distquake_download_areacheck.php";
// download pastquakes
static NSString * const EQNServerUrlDownloadPastQuakes = @"https://srv.earthquakenetwork.it/distquake_download_pastquakes.php";
// download segnalazioni
@@ -64,11 +64,7 @@ static NSString * const EQNServerUrlAlertSimulator = @"https://srv.earthquakenet
#pragma mark - UserDefaults Keys
static NSString * const EQNUserDefaultKeyAlertsShowAllCards = @"EQNetwork.AlertsShowAllCards";
static NSString * const EQNUserDefaultKeySesmicInformations = @"EQNetwork.SeismicInformations";
static NSString * const EQNUserDefaultKeyOneShotShowCountry = @"EQNetwork.OneShot.CountrySelection";
static NSString * const EQNUserDefaultSeismicNetworkCards = @"EQNData.RetiSismiche";
static NSString * const EQNUserDefaultUserReportExpandedView = @"EQNData.UserReportExpandedView";
#pragma mark - NSNotification
@@ -173,14 +169,4 @@ typedef enum : NSInteger {
// Sigla della rete sismica selezionata
#define IMPOSTAZIONE_NAZIONE_RETI_SISMICHE @"IMPOSTAIONE_NAZIONE_RETI_SISMICHE"
// FILTRO ENTI
#define EQN_MAGNITUDO_MINIMA @"EQN_MAGNITUDO_MINIMA"
#define EQN_DISTANZA_MASSIMA @"EQN_DISTANZA_MASSIMA"
#define EQN_ETA_MASSIMA @"EQN_ETA_MASSIMA"
#define EQN_SISMI_FORTI_ABILITATI @"EQN_SISMI_FORTI_ABILITATI"
#define EQN_SISMI_FORTI @"EQN_SISMI_FORTI"
#define EQN_SISMI_QUALSIASI_MAGNITUDO @"EQN_SISMI_QUALSIASI_MAGNITUDO"
#define EQN_SISMI_MODIFICA_IMPOSTAZIONI @"EQN_SISMI_MODIFICA_IMPOSTAZIONI"
#endif /* Costanti_h */
@@ -6,16 +6,12 @@
#import "Costanti.h"
#import "EQNUser.h"
#import "EQNManager.h"
#import "EQNNotificheReteSismiche.h"
#import "EQNNotificheSegnalazioniUtente.h"
#import "EQNSisma.h"
#import "EQNBaseViewController.h"
#import "SettingsBaseViewController.h"
#import "EQNGeneratoreURLServer.h"
#import "ServerRequest.h"
#import "EQNSegnalazione.h"
#import "EQNPastquakes.h"
#import "EQNAllertaSismica.h"
#import "GADTTemplateView.h"
#import "GADTMediumTemplateView.h"
+33 -31
View File
@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.finazzi.distquake.update_server_position</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -18,23 +22,42 @@
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>fb1444404982546319</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<true/>
<key>FacebookAppID</key>
<string>1444404982546319</string>
<key>FacebookAutoLogAppEventsEnabled</key>
<true/>
<key>FacebookClientToken</key>
<string>46c7a338b2bbd2186b2f1c12865b4004</string>
<key>FacebookDisplayName</key>
<string>Earthquake Network</string>
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-0053870219990922~2021960172</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlechromes</string>
<string>comgooglemaps</string>
<string>fbapi</string>
<string>fb-messenger-share-api</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCalendarsUsageDescription</key>
<string>L'accesso al calendario è richiesto per poter salvare le informazioni dei sismi di interesse</string>
<string>L&apos;accesso al calendario è richiesto per poter salvare le informazioni dei sismi di interesse</string>
<key>NSContactsUsageDescription</key>
<string>L'accesso ai contatti è richiesto per poter aggiungere persone agli eventi creati</string>
<string>L&apos;accesso ai contatti è richiesto per poter aggiungere persone agli eventi creati</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string> Ci occorre la tua posizione per inviare messaggi precisi in caso di terremoto</string>
<key>NSLocationAlwaysUsageDescription</key>
@@ -42,7 +65,9 @@
<key>NSLocationWhenInUseUsageDescription</key>
<string> Ci occorre la tua posizione per inviare messaggi precisi in caso di terremoto</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>L'accesso alla libreria è richiesto per poter salvare le immagini generate dall'app</string>
<string>L&apos;accesso alla libreria è richiesto per poter salvare le immagini generate dall&apos;app</string>
<key>NSUserTrackingUsageDescription</key>
<string>Il tracciamento serve a capire se la pubblicità dell&apos;app è efficace</string>
<key>SKAdNetworkItems</key>
<array>
<dict>
@@ -55,6 +80,7 @@
<string>audio</string>
<string>fetch</string>
<string>location</string>
<string>processing</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
@@ -78,31 +104,7 @@
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>fb1444404982546319</string>
</array>
</dict>
</array>
<key>FacebookAppID</key>
<string>1444404982546319</string>
<key>FacebookClientToken</key>
<string>46c7a338b2bbd2186b2f1c12865b4004</string>
<key>FacebookDisplayName</key>
<string>Earthquake Network</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>fbapi</string>
<string>fb-messenger-share-api</string>
</array>
<key>FacebookAutoLogAppEventsEnabled</key>
<true/>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<true/>
<key>NSUserTrackingUsageDescription</key>
<string>Il tracciamento serve a capire se la pubblicità dell'app è efficace</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
</dict>
</plist>
@@ -26,3 +26,14 @@ extension NSDate {
return (self as Date).isBeforeInterval(interval)
}
}
extension CGFloat {
var negative: CGFloat {
-self
}
var x2: CGFloat {
self*2.0
}
}
@@ -12,9 +12,7 @@ import UIKit
extension UIView {
func eqn_applyShadowAndRoundedCorners() {
// rounded corners
layer.cornerRadius = AppTheme.shared.cardCornerRadius
layer.masksToBounds = false
eqn_applyRoundedCorners()
// apply a shadow to the current view
layer.shadowColor = UIColor.black.cgColor
@@ -22,4 +20,10 @@ extension UIView {
layer.shadowOffset = CGSize(width: 0, height: 2)
layer.shadowRadius = 2
}
func eqn_applyRoundedCorners() {
// rounded corners
layer.cornerRadius = AppTheme.shared.cardCornerRadius
layer.masksToBounds = false
}
}
+147
View File
@@ -0,0 +1,147 @@
//
// Log.swift
// Earthquake Network
//
// Created by Andrea Busi on 27/02/25.
// Copyright © 2025 Earthquake Network. All rights reserved.
//
import Foundation
import OSLog
/// Use this protocol to have a base TAG in a Swift class
public protocol Loggable {
static var TAG: String { get }
}
public extension Loggable {
static var TAG: String {
String(describing: Self.self)
}
}
extension UIViewController: Loggable { }
public class Log {
private static let dumpDateFormatter: DateFormatter = {
// create the default date formatter using ISO8601 date format
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return formatter
}()
private static let shared = Log()
// MARK: - Properties
private let maxNumberOfLogsInDump: Int
private let logsLifespanMillis: Int
/// Subsystem for OSLog
private let subsystem: String
/// Logging in everything in a single "APP" category
private let appCategory: String = "APP"
private lazy var logger: os.Logger = {
os.Logger(subsystem: subsystem, category: appCategory)
}()
// MARK: - Init
@objc
public init(
subsystem: String = Bundle.main.bundleIdentifier!,
maxNumberOfLogsInDump: Int = 5000,
logsLifespanMillis: Int = 3 * 24 * 3600 * 1000
) {
self.subsystem = subsystem
self.maxNumberOfLogsInDump = maxNumberOfLogsInDump
self.logsLifespanMillis = logsLifespanMillis
}
// MARK: - Internal
public static func error(tag: String?, _ message: String?, _ functionName: String = #function) {
shared.log(level: .fault, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName)
}
public static func warning(tag: String?, _ message: String?, _ functionName: String = #function) {
shared.log(level: .error, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName)
}
public static func info(tag: String?, _ message: String?, _ functionName: String = #function) {
shared.log(level: .info, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName)
}
public static func debug(tag: String?, _ message: String?, _ functionName: String = #function) {
shared.log(level: .debug, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName)
}
public static func verbose(tag: String?, _ message: String?, _ functionName: String = #function) {
shared.log(level: .debug, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName)
}
public func dumpLog() async -> String {
return (try? await getLogEntries()) ?? ""
}
// MARK: - Private
private func log(level: OSLogType, tag: String, message: String, functionName: String) {
let formattedMessage = "[\(tag)] \(functionName): \(message)"
switch level {
case .fault: logger.fault("\(formattedMessage, privacy: .public)")
case .error: logger.error("\(formattedMessage, privacy: .public)")
case .default: logger.notice("\(formattedMessage, privacy: .public)")
case .info: logger.info("\(formattedMessage, privacy: .public)")
default: logger.debug("\(formattedMessage, privacy: .public)")
}
}
/// Retrieve log entries from a specified time.
/// - Returns: String of log entries, newlines separated
private func getLogEntries() async throws -> String {
let logTask = Task.init(priority: .utility) { () -> String in
let logs = try retrieveLogEntries()
let text = logs
.compactMap { "\(Self.dumpDateFormatter.string(from: $0.date)) [\($0.level)] \($0.composedMessage)" }
.joined(separator: "\n")
return text
}
return try await logTask.value
}
private func retrieveLogEntries() throws -> [OSLogEntryLog] {
// Open the log store.
let logStore = try OSLogStore(scope: .currentProcessIdentifier)
// Fetch log objects from the given time interval
let intervalPosition = logStore.position(date: Date().addingTimeInterval(TimeInterval(-logsLifespanMillis / 1000)))
let allEntries = try logStore.getEntries(at: intervalPosition)
// Filter the log to be relevant for our specific subsystem
// and remove other elements (signposts, etc).
return allEntries
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem == subsystem }
.suffix(maxNumberOfLogsInDump)
}
}
extension OSLogEntryLog.Level: @retroactive CustomStringConvertible {
public var description: String {
switch self {
case .fault: return "FAULT"
case .error: return "ERROR"
case .notice: return "WARNING"
case .info: return "INFO"
case .debug: return "DEBUG"
case .undefined: return "UNDEFINED"
@unknown default:
return "UNKNOWN"
}
}
}
@@ -19,13 +19,47 @@ class AppPreferences: NSObject {
/// Defines if time has to be shown on map annotations in User Reports
var userReportExpandedView: Bool {
get { UserDefaults.standard.bool(forKey: EQNUserDefaultUserReportExpandedView) }
set { UserDefaults.standard.set(newValue, forKey: EQNUserDefaultUserReportExpandedView) }
get { UserDefaults.standard.bool(forKey: UserDefaults.UserReportExpandedView) }
set { UserDefaults.standard.set(newValue, forKey: UserDefaults.UserReportExpandedView) }
}
/// Defines if options has to be shown on seismic cards
@objc
var alertsShowAllCards: Bool {
get { UserDefaults.standard.bool(forKey: EQNUserDefaultKeyAlertsShowAllCards) }
set { UserDefaults.standard.set(newValue, forKey: EQNUserDefaultKeyAlertsShowAllCards) }
get { UserDefaults.standard.bool(forKey: UserDefaults.AlertsShowCardOptions) }
set { UserDefaults.standard.set(newValue, forKey: UserDefaults.AlertsShowCardOptions) }
}
var mapPinStyle: MapPinStyle {
get {
let saved = UserDefaults.standard.integer(forKey: UserDefaults.MapPinStyle)
return MapPinStyle(rawValue: saved) ?? .circle
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: UserDefaults.MapPinStyle)
}
}
var seismicNetworksInformations: [SeismicNetworkTableViewCell.InformationType] {
get {
if let saved = UserDefaults.standard.array(forKey: UserDefaults.SeismicNetworksCardInformations) as? [Int] {
let informations = saved.compactMap { SeismicNetworkTableViewCell.InformationType(rawValue: $0) }
return informations
}
return [.buttons, .distance, .coordinate, .population, .intensityMap]
}
set {
UserDefaults.standard.set(newValue.map { $0.rawValue }, forKey: UserDefaults.SeismicNetworksCardInformations)
}
}
var seismicNetworksCardStyle: SeismicNetworksViewController.CardDisplayType {
get {
let saved = UserDefaults.standard.integer(forKey: UserDefaults.SeismicNetworksCardStyle)
return SeismicNetworksViewController.CardDisplayType(rawValue: saved) ?? .small
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: UserDefaults.SeismicNetworksCardStyle)
}
}
}
@@ -0,0 +1,27 @@
//
// BackgroundTaskIdentifiable.swift
// Earthquake Network
//
// Created by Andrea Busi on 11/09/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
import BackgroundTasks
protocol BackgroundTaskIdentifiable {
typealias TaskCompletion = (_ success: Bool) -> Void
static var identifier: String { get }
static var interval: TimeInterval { get }
init()
func handle(_ task: BGTask, completion: @escaping TaskCompletion)
func exipration()
}
extension BackgroundTaskIdentifiable {
func exipration() { }
}
@@ -0,0 +1,81 @@
//
// BackgroundTaskManager.swift
// Earthquake Network
//
// Created by Andrea Busi on 16/08/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
import BackgroundTasks
@objc
class BackgroundTaskManager: NSObject {
@objc
static let shared = BackgroundTaskManager()
private let identifiers: [BackgroundTaskIdentifiable.Type] = [UpdateUserLocationTask.self]
// MARK: - Public
@objc
func registerTasks() {
identifiers
.forEach { taskIdentifiable in
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifiable.identifier, using: DispatchQueue.global()) { [weak self] task in
guard let appTask = task as? BGAppRefreshTask else { return }
self?.handleTask(appTask, with: taskIdentifiable)
self?.scheduleTaskRequest(for: taskIdentifiable)
}
}
}
// MARK: - Public
@objc
func scheduleUpdateServerPosition() {
scheduleTaskRequest(for: UpdateUserLocationTask.self)
}
// MARK: - Private
private func scheduleTaskRequest(
for taskIdentifiable: BackgroundTaskIdentifiable.Type
) {
let request = BGAppRefreshTaskRequest(identifier: taskIdentifiable.identifier)
// Fetch no earlier than X minutes from now
request.earliestBeginDate = Date(timeIntervalSinceNow: taskIdentifiable.interval)
//request.requiresNetworkConnectivity = true
do {
try BGTaskScheduler.shared.submit(request)
print("[BackgroundTaskManager] Background task scheduler submitted")
} catch {
print("[BackgroundTaskManager] Could not schedule background taksk. Error: \(error)")
}
}
private func handleTask(
_ appTask: BGAppRefreshTask,
with taskIdentifiable: BackgroundTaskIdentifiable.Type
) {
// create a task
let task = taskIdentifiable.init()
// Provide the background task with an expiration handler that cancels the operation.
appTask.expirationHandler = {
task.exipration()
}
// Handle workload
task.handle(appTask) { success in
// Inform the system that the background task is complete
// when the operation completes.
appTask.setTaskCompleted(success: success)
}
}
}
@@ -0,0 +1,32 @@
//
// EQNBackgroundPosition.swift
// Earthquake Network
//
// Created by Andrea Busi on 16/08/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
import CoreLocation
struct EQNBackgroundPosition: Codable {
let date: Date
private let latitude: Double
private let longitude: Double
var coordinate: CLLocationCoordinate2D {
.init(latitude: latitude, longitude: longitude)
}
let request: Bool?
init(
date: Date,
coordinate: CLLocationCoordinate2D,
request: Bool?
) {
self.date = date
self.latitude = coordinate.latitude
self.longitude = coordinate.longitude
self.request = request
}
}
@@ -0,0 +1,66 @@
//
// EQNDebugHelper.swift
// Earthquake Network
//
// Created by Andrea Busi on 14/07/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
@objc
class EQNBackgroundPositionDebugHelper: NSObject {
@objc
static let shared = EQNBackgroundPositionDebugHelper()
private var timers: [String: Timer] = [:]
@objc
var isEnabled: Bool { false }
// MARK: - Public
@objc
func printPositions(
interval: TimeInterval = 2.0,
repeats: Bool = true
) {
let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { timer in
let current = EQNUserData.shared.lastLocation?.coordinate
print("[EQNDebugHelper] Current | lat: \(current?.latitude ?? 0) - lon: \(current?.longitude ?? 0)")
let saved = EQNUser.default().lastPosition?.coordinate
print("[EQNDebugHelper] Saved | lat: \(saved?.latitude ?? 0) - lon: \(saved?.longitude ?? 0)")
}
timers["positions"] = timer
}
// MARK: - Class
func savePosition(
coordinate: CLLocationCoordinate2D,
requestSuccess: Bool
) {
var positions = loadPosition()
positions.append(.init(date: Date(), coordinate: coordinate, request: requestSuccess))
if let data = try? JSONEncoder().encode(positions) {
UserDefaults.standard.set(data, forKey: "BackgroundPositions")
}
}
func resetPositions() {
UserDefaults.standard.removeObject(forKey: "BackgroundPositions")
}
func loadPosition() -> [EQNBackgroundPosition] {
guard let data = UserDefaults.standard.object(forKey: "BackgroundPositions") as? Data else {
return []
}
let positions = try? JSONDecoder().decode([EQNBackgroundPosition].self, from: data)
return positions ?? []
}
}
@@ -0,0 +1,95 @@
//
// UpdateUserLocationTask.swift
// Earthquake Network
//
// Created by Andrea Busi on 16/08/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
import BackgroundTasks
import CoreLocation
final class UpdateUserLocationTask: NSObject, BackgroundTaskIdentifiable {
static var identifier: String {
"com.finazzi.distquake.update_server_position"
}
static var interval: TimeInterval {
5 * 60
}
static let shared = UpdateUserLocationTask()
private let debugHelper = EQNBackgroundPositionDebugHelper()
// MARK: - Internal
private lazy var locationManager: CLLocationManager = {
let manager = CLLocationManager()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
manager.allowsBackgroundLocationUpdates = true
manager.pausesLocationUpdatesAutomatically = false
return manager
}()
var appTaskCompletion: BackgroundTaskIdentifiable.TaskCompletion?
func handle(_ task: BGTask, completion: @escaping (_ success: Bool) -> Void) {
self.appTaskCompletion = completion
// ricaviamo la posizione corrente dell'utente
if let location = locationManager.location {
complete(with: location)
} else {
locationManager.requestLocation()
}
}
func exipration() {
locationManager.stopUpdatingLocation()
failed()
}
// MARK: - Private
private func complete(with location: CLLocation) {
// send position to cloud
let url = EQNGeneratoreURLServer.urlPosizione(withLocation: location.coordinate)
ServerRequest.default().inviaInformazioniAlServer(with: url, richiesta: .posizione) { result in
if self.debugHelper.isEnabled {
self.debugHelper.savePosition(coordinate: location.coordinate, requestSuccess: true)
}
self.appTaskCompletion?(true)
} failure: { error in
if self.debugHelper.isEnabled {
self.debugHelper.savePosition(coordinate: location.coordinate, requestSuccess: false)
}
self.appTaskCompletion?(false)
}
}
private func failed() {
appTaskCompletion?(false)
}
}
extension UpdateUserLocationTask: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
complete(with: location)
} else {
failed()
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("[UpdateUserLocationTask] Location manager failed. Error: \(error.localizedDescription)")
// nope, but mandatory
failed()
}
}
@@ -14,7 +14,7 @@ public class EQNAppearanceCommand: EQNCommandProtocol {
// MARK: - Public
func execute() {
print("EQNAppearanceCommand: start execute")
print("[EQNAppearanceCommand] Start execute")
applyAppearance()
}
@@ -24,30 +24,21 @@ public class EQNAppearanceCommand: EQNCommandProtocol {
private func applyAppearance() {
// UINavigationBar
let proxyNavBar = UINavigationBar.appearance(whenContainedInInstancesOf: [UINavigationController.self])
if #available(iOS 13.0, *) {
let navAppearance = UINavigationBarAppearance()
navAppearance.configureWithOpaqueBackground()
navAppearance.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: AppTheme.Colors.darkGray
]
navAppearance.largeTitleTextAttributes = [
NSAttributedString.Key.foregroundColor: AppTheme.Colors.darkGray
]
navAppearance.backgroundColor = AppTheme.Colors.primary
navAppearance.shadowColor = UIColor.clear
let navAppearance = UINavigationBarAppearance()
navAppearance.configureWithOpaqueBackground()
navAppearance.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: AppTheme.Colors.darkGray
]
navAppearance.largeTitleTextAttributes = [
NSAttributedString.Key.foregroundColor: AppTheme.Colors.darkGray
]
navAppearance.backgroundColor = AppTheme.Colors.navBar
navAppearance.shadowColor = UIColor.clear
proxyNavBar.isTranslucent = false
proxyNavBar.tintColor = AppTheme.Colors.darkGray
proxyNavBar.standardAppearance = navAppearance
proxyNavBar.scrollEdgeAppearance = navAppearance
} else {
proxyNavBar.tintColor = AppTheme.Colors.darkGray
proxyNavBar.isTranslucent = false
proxyNavBar.barTintColor = AppTheme.Colors.primary
proxyNavBar.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: UIColor.white
]
}
proxyNavBar.isTranslucent = false
proxyNavBar.tintColor = AppTheme.Colors.darkGray
proxyNavBar.standardAppearance = navAppearance
proxyNavBar.scrollEdgeAppearance = navAppearance
let proxyTabBar = UITabBar.appearance()
proxyTabBar.tintColor = AppTheme.Colors.red

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