Commit dcd92a0e by Demid Merzlyakov

Alerts: fetching and parsing.

parent 3d8b7b8f
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
87C1720D25FF870600DA3464 /* GeoNamesPlace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C1720C25FF870600DA3464 /* GeoNamesPlace.swift */; }; 87C1720D25FF870600DA3464 /* GeoNamesPlace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C1720C25FF870600DA3464 /* GeoNamesPlace.swift */; };
87C1721025FF874B00DA3464 /* PartialLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C1720F25FF874B00DA3464 /* PartialLocation.swift */; }; 87C1721025FF874B00DA3464 /* PartialLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C1720F25FF874B00DA3464 /* PartialLocation.swift */; };
87C1724925FF94F400DA3464 /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C1724825FF94F400DA3464 /* ConfigManager.swift */; }; 87C1724925FF94F400DA3464 /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C1724825FF94F400DA3464 /* ConfigManager.swift */; };
87D81582262EFC9B0015A6D1 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D81581262EFC9A0015A6D1 /* Notifications.swift */; };
CD1237C3255D5C5900C98139 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1237C2255D5C5900C98139 /* AppDelegate.swift */; }; CD1237C3255D5C5900C98139 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1237C2255D5C5900C98139 /* AppDelegate.swift */; };
CD1237CC255D5C5C00C98139 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD1237CB255D5C5C00C98139 /* Assets.xcassets */; }; CD1237CC255D5C5C00C98139 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD1237CB255D5C5C00C98139 /* Assets.xcassets */; };
CD1237F1255D83C500C98139 /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1237F0255D83C500C98139 /* UIColor+Hex.swift */; }; CD1237F1255D83C500C98139 /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1237F0255D83C500C98139 /* UIColor+Hex.swift */; };
...@@ -117,6 +118,8 @@ ...@@ -117,6 +118,8 @@
CDF4808F261727E00076E9F5 /* CLAuthorizationStatus+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF4808E261727E00076E9F5 /* CLAuthorizationStatus+Localized.swift */; }; CDF4808F261727E00076E9F5 /* CLAuthorizationStatus+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF4808E261727E00076E9F5 /* CLAuthorizationStatus+Localized.swift */; };
CDF48092261729680076E9F5 /* UIApplication+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF48091261729680076E9F5 /* UIApplication+Settings.swift */; }; CDF48092261729680076E9F5 /* UIApplication+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF48091261729680076E9F5 /* UIApplication+Settings.swift */; };
CDF9BF8E26133D050037847D /* LocationSearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF9BF8D26133D050037847D /* LocationSearchCoordinator.swift */; }; CDF9BF8E26133D050037847D /* LocationSearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF9BF8D26133D050037847D /* LocationSearchCoordinator.swift */; };
CE04561F26282325003D252B /* NWSCurrentEventsReponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE04561E26282325003D252B /* NWSCurrentEventsReponse.swift */; };
CE0456242629C04C003D252B /* NWSAlertsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0456232629C04C003D252B /* NWSAlertsManager.swift */; };
CE13B72826245CE2007CBD4D /* AirQuality.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13B72726245CE2007CBD4D /* AirQuality.swift */; }; CE13B72826245CE2007CBD4D /* AirQuality.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13B72726245CE2007CBD4D /* AirQuality.swift */; };
CE13B72B26245D0D007CBD4D /* HealthStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13B72A26245D0D007CBD4D /* HealthStatus.swift */; }; CE13B72B26245D0D007CBD4D /* HealthStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13B72A26245D0D007CBD4D /* HealthStatus.swift */; };
CE13B72E26245D42007CBD4D /* Pollutant.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13B72D26245D42007CBD4D /* Pollutant.swift */; }; CE13B72E26245D42007CBD4D /* Pollutant.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13B72D26245D42007CBD4D /* Pollutant.swift */; };
...@@ -151,6 +154,9 @@ ...@@ -151,6 +154,9 @@
CE13B88F26248A77007CBD4D /* GoogleService-Info-Staging.plist in Resources */ = {isa = PBXBuildFile; fileRef = CE13B88D26248A77007CBD4D /* GoogleService-Info-Staging.plist */; }; CE13B88F26248A77007CBD4D /* GoogleService-Info-Staging.plist in Resources */ = {isa = PBXBuildFile; fileRef = CE13B88D26248A77007CBD4D /* GoogleService-Info-Staging.plist */; };
CE13B97B2626FB11007CBD4D /* PSMLocationSDK.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE13B7DC262478E7007CBD4D /* PSMLocationSDK.xcframework */; }; CE13B97B2626FB11007CBD4D /* PSMLocationSDK.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE13B7DC262478E7007CBD4D /* PSMLocationSDK.xcframework */; };
CE13B97C2626FB11007CBD4D /* PSMLocationSDK.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE13B7DC262478E7007CBD4D /* PSMLocationSDK.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE13B97C2626FB11007CBD4D /* PSMLocationSDK.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE13B7DC262478E7007CBD4D /* PSMLocationSDK.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE13B98226272A1F007CBD4D /* AppNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13B98126272A1F007CBD4D /* AppNotification.swift */; };
CE13B98526272E18007CBD4D /* WeatherAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13B98426272E18007CBD4D /* WeatherAlert.swift */; };
CE13B98726273236007CBD4D /* NWSAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13B98626273236007CBD4D /* NWSAlert.swift */; };
CE28474F26159857006C8DC5 /* HealthSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE28474E26159857006C8DC5 /* HealthSource.swift */; }; CE28474F26159857006C8DC5 /* HealthSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE28474E26159857006C8DC5 /* HealthSource.swift */; };
CE28475226159A32006C8DC5 /* BlendHealthModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE28475126159A32006C8DC5 /* BlendHealthModels.swift */; }; CE28475226159A32006C8DC5 /* BlendHealthModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE28475126159A32006C8DC5 /* BlendHealthModels.swift */; };
CE28475D2615A5B3006C8DC5 /* Health.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE28475C2615A5B3006C8DC5 /* Health.swift */; }; CE28475D2615A5B3006C8DC5 /* Health.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE28475C2615A5B3006C8DC5 /* Health.swift */; };
...@@ -237,6 +243,7 @@ ...@@ -237,6 +243,7 @@
87C1720C25FF870600DA3464 /* GeoNamesPlace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoNamesPlace.swift; sourceTree = "<group>"; }; 87C1720C25FF870600DA3464 /* GeoNamesPlace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoNamesPlace.swift; sourceTree = "<group>"; };
87C1720F25FF874B00DA3464 /* PartialLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialLocation.swift; sourceTree = "<group>"; }; 87C1720F25FF874B00DA3464 /* PartialLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialLocation.swift; sourceTree = "<group>"; };
87C1724825FF94F400DA3464 /* ConfigManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigManager.swift; sourceTree = "<group>"; }; 87C1724825FF94F400DA3464 /* ConfigManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigManager.swift; sourceTree = "<group>"; };
87D81581262EFC9A0015A6D1 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
C8C576F6184B547435CFF0F3 /* Pods-1Weather.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-1Weather.debug.xcconfig"; path = "Target Support Files/Pods-1Weather/Pods-1Weather.debug.xcconfig"; sourceTree = "<group>"; }; C8C576F6184B547435CFF0F3 /* Pods-1Weather.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-1Weather.debug.xcconfig"; path = "Target Support Files/Pods-1Weather/Pods-1Weather.debug.xcconfig"; sourceTree = "<group>"; };
CD1237BF255D5C5900C98139 /* 1Weather.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = 1Weather.app; sourceTree = BUILT_PRODUCTS_DIR; }; CD1237BF255D5C5900C98139 /* 1Weather.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = 1Weather.app; sourceTree = BUILT_PRODUCTS_DIR; };
CD1237C2255D5C5900C98139 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; CD1237C2255D5C5900C98139 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
...@@ -345,6 +352,8 @@ ...@@ -345,6 +352,8 @@
CDF4808E261727E00076E9F5 /* CLAuthorizationStatus+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLAuthorizationStatus+Localized.swift"; sourceTree = "<group>"; }; CDF4808E261727E00076E9F5 /* CLAuthorizationStatus+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLAuthorizationStatus+Localized.swift"; sourceTree = "<group>"; };
CDF48091261729680076E9F5 /* UIApplication+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Settings.swift"; sourceTree = "<group>"; }; CDF48091261729680076E9F5 /* UIApplication+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Settings.swift"; sourceTree = "<group>"; };
CDF9BF8D26133D050037847D /* LocationSearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSearchCoordinator.swift; sourceTree = "<group>"; }; CDF9BF8D26133D050037847D /* LocationSearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSearchCoordinator.swift; sourceTree = "<group>"; };
CE04561E26282325003D252B /* NWSCurrentEventsReponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSCurrentEventsReponse.swift; sourceTree = "<group>"; };
CE0456232629C04C003D252B /* NWSAlertsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSAlertsManager.swift; sourceTree = "<group>"; };
CE13B72726245CE2007CBD4D /* AirQuality.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirQuality.swift; sourceTree = "<group>"; }; CE13B72726245CE2007CBD4D /* AirQuality.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirQuality.swift; sourceTree = "<group>"; };
CE13B72A26245D0D007CBD4D /* HealthStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthStatus.swift; sourceTree = "<group>"; }; CE13B72A26245D0D007CBD4D /* HealthStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthStatus.swift; sourceTree = "<group>"; };
CE13B72D26245D42007CBD4D /* Pollutant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pollutant.swift; sourceTree = "<group>"; }; CE13B72D26245D42007CBD4D /* Pollutant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pollutant.swift; sourceTree = "<group>"; };
...@@ -379,6 +388,9 @@ ...@@ -379,6 +388,9 @@
CE13B809262480B3007CBD4D /* Scheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scheduler.swift; sourceTree = "<group>"; }; CE13B809262480B3007CBD4D /* Scheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scheduler.swift; sourceTree = "<group>"; };
CE13B88C26248A77007CBD4D /* GoogleService-Info-Production.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info-Production.plist"; sourceTree = "<group>"; }; CE13B88C26248A77007CBD4D /* GoogleService-Info-Production.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info-Production.plist"; sourceTree = "<group>"; };
CE13B88D26248A77007CBD4D /* GoogleService-Info-Staging.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info-Staging.plist"; sourceTree = "<group>"; }; CE13B88D26248A77007CBD4D /* GoogleService-Info-Staging.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info-Staging.plist"; sourceTree = "<group>"; };
CE13B98126272A1F007CBD4D /* AppNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotification.swift; sourceTree = "<group>"; };
CE13B98426272E18007CBD4D /* WeatherAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherAlert.swift; sourceTree = "<group>"; };
CE13B98626273236007CBD4D /* NWSAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSAlert.swift; sourceTree = "<group>"; };
CE28474E26159857006C8DC5 /* HealthSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthSource.swift; sourceTree = "<group>"; }; CE28474E26159857006C8DC5 /* HealthSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthSource.swift; sourceTree = "<group>"; };
CE28475126159A32006C8DC5 /* BlendHealthModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlendHealthModels.swift; sourceTree = "<group>"; }; CE28475126159A32006C8DC5 /* BlendHealthModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlendHealthModels.swift; sourceTree = "<group>"; };
CE28475C2615A5B3006C8DC5 /* Health.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Health.swift; sourceTree = "<group>"; }; CE28475C2615A5B3006C8DC5 /* Health.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Health.swift; sourceTree = "<group>"; };
...@@ -985,6 +997,34 @@ ...@@ -985,6 +997,34 @@
path = Firebase; path = Firebase;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CE13B97D262725F0007CBD4D /* Notifications */ = {
isa = PBXGroup;
children = (
CE13B98026272A13007CBD4D /* Model */,
CE0456232629C04C003D252B /* NWSAlertsManager.swift */,
);
path = Notifications;
sourceTree = "<group>";
};
CE13B98026272A13007CBD4D /* Model */ = {
isa = PBXGroup;
children = (
CE13B98626273236007CBD4D /* NWSAlert.swift */,
CE04561E26282325003D252B /* NWSCurrentEventsReponse.swift */,
);
path = Model;
sourceTree = "<group>";
};
CE13B98326272A59007CBD4D /* Notifications */ = {
isa = PBXGroup;
children = (
CE13B98126272A1F007CBD4D /* AppNotification.swift */,
CE13B98426272E18007CBD4D /* WeatherAlert.swift */,
87D81581262EFC9A0015A6D1 /* Notifications.swift */,
);
path = Notifications;
sourceTree = "<group>";
};
CE28474C261597EB006C8DC5 /* Health */ = { CE28474C261597EB006C8DC5 /* Health */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
...@@ -1114,10 +1154,11 @@ ...@@ -1114,10 +1154,11 @@
CEAFF09925DFC78200DF4EBF /* Network */ = { CEAFF09925DFC78200DF4EBF /* Network */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CE28474C261597EB006C8DC5 /* Health */,
87C1724825FF94F400DA3464 /* ConfigManager.swift */, 87C1724825FF94F400DA3464 /* ConfigManager.swift */,
87C171F325FF7A4000DA3464 /* PopularCitiesManager.swift */, 87C171F325FF7A4000DA3464 /* PopularCitiesManager.swift */,
87C171F125FF7A3300DA3464 /* Weather */, 87C171F125FF7A3300DA3464 /* Weather */,
CE28474C261597EB006C8DC5 /* Health */,
CE13B97D262725F0007CBD4D /* Notifications */,
); );
path = Network; path = Network;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -1141,6 +1182,7 @@ ...@@ -1141,6 +1182,7 @@
CEAFF08225DFC67F00DF4EBF /* Location.swift */, CEAFF08225DFC67F00DF4EBF /* Location.swift */,
CE28475726159B78006C8DC5 /* Weather */, CE28475726159B78006C8DC5 /* Weather */,
CE28475926159B85006C8DC5 /* Health */, CE28475926159B85006C8DC5 /* Health */,
CE13B98326272A59007CBD4D /* Notifications */,
); );
path = ModelObjects; path = ModelObjects;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -1531,6 +1573,7 @@ ...@@ -1531,6 +1573,7 @@
CD86246925E672A20097F3FB /* PrecipButton.swift in Sources */, CD86246925E672A20097F3FB /* PrecipButton.swift in Sources */,
CD3F6E6C25FA5A90002DB99B /* PeriodButtonProtocol.swift in Sources */, CD3F6E6C25FA5A90002DB99B /* PeriodButtonProtocol.swift in Sources */,
CEDE4E8225EEFD56007457E9 /* WdtWeatherCode.swift in Sources */, CEDE4E8225EEFD56007457E9 /* WdtWeatherCode.swift in Sources */,
CE04561F26282325003D252B /* NWSCurrentEventsReponse.swift in Sources */,
CD1DDD30260218AE00AC62B2 /* DaysControlView.swift in Sources */, CD1DDD30260218AE00AC62B2 /* DaysControlView.swift in Sources */,
CE9D181625ECB8370028D9D7 /* MulticastDelegate.swift in Sources */, CE9D181625ECB8370028D9D7 /* MulticastDelegate.swift in Sources */,
CE578FE725FB415F00E8B85D /* LocationsViewModel.swift in Sources */, CE578FE725FB415F00E8B85D /* LocationsViewModel.swift in Sources */,
...@@ -1550,6 +1593,7 @@ ...@@ -1550,6 +1593,7 @@
CD593BD32608BC3F00C93428 /* ForecastDayCell.swift in Sources */, CD593BD32608BC3F00C93428 /* ForecastDayCell.swift in Sources */,
CD4742D0261200500061AC95 /* TodayAlertCell.swift in Sources */, CD4742D0261200500061AC95 /* TodayAlertCell.swift in Sources */,
CD15DB4225DA806C00024727 /* TodayForecastTimePeriodCell.swift in Sources */, CD15DB4225DA806C00024727 /* TodayForecastTimePeriodCell.swift in Sources */,
CE0456242629C04C003D252B /* NWSAlertsManager.swift in Sources */,
CE8962AB26175DF500CA274A /* CorePollutant.swift in Sources */, CE8962AB26175DF500CA274A /* CorePollutant.swift in Sources */,
CEC5276025E92DDA00DA58A5 /* WdtHourlySummary.swift in Sources */, CEC5276025E92DDA00DA58A5 /* WdtHourlySummary.swift in Sources */,
CDE18DCA25D165F100C80ED9 /* UITabBarController+Append.swift in Sources */, CDE18DCA25D165F100C80ED9 /* UITabBarController+Append.swift in Sources */,
...@@ -1576,6 +1620,7 @@ ...@@ -1576,6 +1620,7 @@
CE9F01B8261B2DB6009BA500 /* _CoreAppData.swift in Sources */, CE9F01B8261B2DB6009BA500 /* _CoreAppData.swift in Sources */,
CDE18DCD25D1666700C80ED9 /* ForecastCoordinator.swift in Sources */, CDE18DCD25D1666700C80ED9 /* ForecastCoordinator.swift in Sources */,
CDC6126625E9085600188DA7 /* GraphLine.swift in Sources */, CDC6126625E9085600188DA7 /* GraphLine.swift in Sources */,
CE13B98726273236007CBD4D /* NWSAlert.swift in Sources */,
CE13B819262480B3007CBD4D /* A9Cache.swift in Sources */, CE13B819262480B3007CBD4D /* A9Cache.swift in Sources */,
CE578FE625FB415F00E8B85D /* LocationViewController.swift in Sources */, CE578FE625FB415F00E8B85D /* LocationViewController.swift in Sources */,
CD86246525E66E8A0097F3FB /* PrecipitationCell.swift in Sources */, CD86246525E66E8A0097F3FB /* PrecipitationCell.swift in Sources */,
...@@ -1605,12 +1650,15 @@ ...@@ -1605,12 +1650,15 @@
CD32CE12260C77B400235081 /* MenuBuyButton.swift in Sources */, CD32CE12260C77B400235081 /* MenuBuyButton.swift in Sources */,
CD86246C25E6826A0097F3FB /* InnerShadowLayer.swift in Sources */, CD86246C25E6826A0097F3FB /* InnerShadowLayer.swift in Sources */,
CE578FE525FB415F00E8B85D /* CityCell.swift in Sources */, CE578FE525FB415F00E8B85D /* CityCell.swift in Sources */,
87D81582262EFC9B0015A6D1 /* Notifications.swift in Sources */,
CEAFF0A325E0FF0800DF4EBF /* LocationManager.swift in Sources */, CEAFF0A325E0FF0800DF4EBF /* LocationManager.swift in Sources */,
CE13B7E226247BF9007CBD4D /* UserDefaultsValue.swift in Sources */, CE13B7E226247BF9007CBD4D /* UserDefaultsValue.swift in Sources */,
CEAD00A12577B2D5003596AD /* StuffThatIsPresentInTheMainProject.swift in Sources */, CEAD00A12577B2D5003596AD /* StuffThatIsPresentInTheMainProject.swift in Sources */,
CE13B98226272A1F007CBD4D /* AppNotification.swift in Sources */,
CEDE4E8925EEFFEF007457E9 /* WdtDayNight.swift in Sources */, CEDE4E8925EEFFEF007457E9 /* WdtDayNight.swift in Sources */,
CE13B820262480B3007CBD4D /* AdLogger.swift in Sources */, CE13B820262480B3007CBD4D /* AdLogger.swift in Sources */,
CDF48092261729680076E9F5 /* UIApplication+Settings.swift in Sources */, CDF48092261729680076E9F5 /* UIApplication+Settings.swift in Sources */,
CE13B98526272E18007CBD4D /* WeatherAlert.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
......
...@@ -21,6 +21,7 @@ public class LocationManager { ...@@ -21,6 +21,7 @@ public class LocationManager {
private let weatherUpdateSource: WeatherSource private let weatherUpdateSource: WeatherSource
private let healthSource: HealthSource private let healthSource: HealthSource
private let nwsAlertsManager: NWSAlertsManager
private let storage: Storage private let storage: Storage
private var defaultLocation = Location(deviceLocation: false, private var defaultLocation = Location(deviceLocation: false,
coordinates: .init(latitude: 37.3230, longitude: -122.0322), // Cupertino coordinates: .init(latitude: 37.3230, longitude: -122.0322), // Cupertino
...@@ -149,13 +150,14 @@ public class LocationManager { ...@@ -149,13 +150,14 @@ public class LocationManager {
} }
} }
public static let shared = LocationManager(weatherUpdateSource: WdtWeatherSource(), healthSource: BlendHealthSource(), storage: CoreDataStorage()) public static let shared = LocationManager(weatherUpdateSource: WdtWeatherSource(), healthSource: BlendHealthSource(), nwsAlertsManager: NWSAlertsManager(), storage: CoreDataStorage())
public let maxLocationsCount = 12 public let maxLocationsCount = 12
public init(weatherUpdateSource: WeatherSource, healthSource: HealthSource, storage: Storage) { public init(weatherUpdateSource: WeatherSource, healthSource: HealthSource, nwsAlertsManager: NWSAlertsManager, storage: Storage) {
self.weatherUpdateSource = weatherUpdateSource self.weatherUpdateSource = weatherUpdateSource
self.healthSource = healthSource self.healthSource = healthSource
self.deviceLocationMonitor = DeviceLocationMonitor() self.deviceLocationMonitor = DeviceLocationMonitor()
self.nwsAlertsManager = nwsAlertsManager
self.storage = storage self.storage = storage
self.deviceLocationMonitor.delegate = self self.deviceLocationMonitor.delegate = self
...@@ -181,6 +183,7 @@ public class LocationManager { ...@@ -181,6 +183,7 @@ public class LocationManager {
log.info("Update all: update default location if needed.") log.info("Update all: update default location if needed.")
updateWeather(for: defaultLocation, updateType: .full) updateWeather(for: defaultLocation, updateType: .full)
updateHealth(for: defaultLocation) updateHealth(for: defaultLocation)
updateNotifications(for: defaultLocation)
return return
} }
log.info("Update all \(locations.count) locations if needed...") log.info("Update all \(locations.count) locations if needed...")
...@@ -192,6 +195,7 @@ public class LocationManager { ...@@ -192,6 +195,7 @@ public class LocationManager {
updateWeather(for: location, updateType: .preferIncremental) updateWeather(for: location, updateType: .preferIncremental)
} }
updateHealth(for: location) updateHealth(for: location)
updateNotifications(for: location)
} }
} }
...@@ -229,6 +233,41 @@ public class LocationManager { ...@@ -229,6 +233,41 @@ public class LocationManager {
} }
} }
public func updateNotifications(for location: Location) {
if let lastTimeUpdated = location.notifications?.updatedAt {
guard Date().timeIntervalSince(lastTimeUpdated) >= nwsAlertsManager.updateInterval else {
log.info("Update notifications (\(location)): fresh enough (last updated at \(lastTimeUpdated), skip update")
return
}
}
log.info("Update notifications for \(location)")
nwsAlertsManager.fetchActiveAlerts(for: location) { [weak self] (alerts, error) in
guard let self = self else { return }
guard let alerts = alerts else {
if let error = error {
self.log.error("Update notifications (\(location)) error: \(error)")
}
else {
self.log.error("Update notifications (\(location)) error: unknown error")
}
return
}
let updatedNotifications = Notifications(updatedAt: Date(), nwsAlers: alerts)
self.log.info("Update notifications (\(location)): success.")
DispatchQueue.main.async {
if let indexToUpdate = self.locations.firstIndex(where: { $0 == location }) {
self.locations[indexToUpdate].notifications = updatedNotifications
}
else if self.defaultLocation == location {
self.defaultLocation.notifications = updatedNotifications
}
else {
self.log.warning("Update notifications: Failed to find location after update. Maybe it was deleted while the update was performed. Maybe something went wrong. Location: \(location)")
}
}
}
}
public func updateWeather(for location: Location?, updateType: WeatherUpdateType) { public func updateWeather(for location: Location?, updateType: WeatherUpdateType) {
guard let location = location else { guard let location = location else {
log.warning("Update weather: empty location.") log.warning("Update weather: empty location.")
......
...@@ -15,6 +15,7 @@ public struct Location { ...@@ -15,6 +15,7 @@ public struct Location {
public var lastWeatherUpdateDate: Date? public var lastWeatherUpdateDate: Date?
public var coordinates: CLLocationCoordinate2D? public var coordinates: CLLocationCoordinate2D?
public var imageName: String? = "ny_bridge" //we'll possibly need to switch to URL public var imageName: String? = "ny_bridge" //we'll possibly need to switch to URL
public var notifications: Notifications?
public var countryCode: String? { public var countryCode: String? {
didSet { didSet {
......
//
// Notification.swift
// 1Weather
//
// Created by Demid Merzlyakov on 14.04.2021.
//
import Foundation
protocol AppNotification {
var date: Date { get }
var title: String { get }
var text: String? { get }
var location: Location? { get }
var expires: Date? { get }
var expired: Bool { get }
}
extension AppNotification {
var expired: Bool {
guard let expires = expires else {
return false
}
return Date() < expires
}
}
//
// Notifications.swift
// 1Weather
//
// Created by Demid Merzlyakov on 20.04.2021.
//
import Foundation
public struct Notifications {
public let updatedAt: Date
public let nwsAlers: [NWSAlert]
}
//
// NWSAlert.swift
// 1Weather
//
// Created by Demid Merzlyakov on 14.04.2021.
//
import Foundation
struct WeatherAlert: AppNotification {
var date: Date
var title: String
var text: String?
var location: Location?
var expires: Date?
var whatText: String?
var whereText: String?
var whenText: String?
var impactText: String?
var instructionsText: String?
var targetAreaText: String?
}
...@@ -29,9 +29,9 @@ public class BlendHealthSource: HealthSource { ...@@ -29,9 +29,9 @@ public class BlendHealthSource: HealthSource {
private static let blendAPIKeyHeaderName = "blend-api-key" private static let blendAPIKeyHeaderName = "blend-api-key"
private static let blendAPIKey = "0imfnc8mVLWwsAawjYr4Rx-Af50DDqtlx" private static let blendAPIKey = "0imfnc8mVLWwsAawjYr4Rx-Af50DDqtlx"
#if DEBUG #if DEBUG
public var healthUpdateInterval: TimeInterval = TimeInterval(2 * 60) // 2 minutes public let healthUpdateInterval = TimeInterval(2 * 60) // 2 minutes
#else #else
public var healthUpdateInterval: TimeInterval = TimeInterval(15 * 60) // 15 minutes public let healthUpdateInterval = TimeInterval(15 * 60) // 15 minutes
#endif #endif
/// This queue is needed to synchronize access to locationsBeingUpdated. Also, to make logging more clear. /// This queue is needed to synchronize access to locationsBeingUpdated. Also, to make logging more clear.
private let internalQueue: OperationQueue = { private let internalQueue: OperationQueue = {
......
//
// NWSAlert.swift
// 1Weather
//
// Created by Demid Merzlyakov on 14.04.2021.
//
import Foundation
public enum NWSSeverityLevel: String, Codable {
case warning = "1"
case watch = "2"
case advisory = "3"
}
public struct NWSAlert: Codable, Equatable, Hashable {
public let weatherID: String
public let messageID: String
public let messageURL: String
public let severityLevel: NWSSeverityLevel
public let description: String
public let expires: Date //expiresUTC="2012-09-25 01:00:00"
/// This is the contents of the messageURL. Fetched separately.
public var weatherMessage: String? = nil
/// This property is set by NWSAlertManager after decoding the response.
public var city: String = ""
// Used to encode/decode a JSON object to send/recieve data with the server
private enum CodingKeys: String, CodingKey {
case weatherID
case messageID
case severityLevel
case description
case expires = "expiresUTC"
case messageURL
}
public var expired: Bool {
return Date() < expires
}
// MARK: Equatable implementation
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.weatherID == rhs.weatherID &&
lhs.messageID == rhs.messageID &&
lhs.messageURL == rhs.messageURL &&
lhs.severityLevel == rhs.severityLevel &&
lhs.description == rhs.description &&
lhs.expires == rhs.expires
// do not compare weatherMessage & city
}
public func hash(into hasher: inout Hasher) {
hasher.combine(weatherID)
hasher.combine(messageID)
hasher.combine(messageURL)
hasher.combine(severityLevel)
hasher.combine(description)
hasher.combine(expires)
}
}
//
// NWSCurrentEventsReponse.swift
// 1Weather
//
// Created by Demid Merzlyakov on 15.04.2021.
//
import Foundation
struct NWSCurrentEventsReponse: Codable {
struct Wrapper: Codable {
let events: [NWSAlert]?
}
let status: String
let events: NWSCurrentEventsReponse.Wrapper?
}
//
// NWSAlertsManager.swift
// 1Weather
//
// Created by Demid Merzlyakov on 16.04.2021.
//
import Foundation
public enum NWSError: Error {
case insufficientLocationInfo
case alreadyBeingUpdated
case badUrl
case networkError(Error?)
case badServerResponse(Error?)
}
public class NWSAlertsManager {
public typealias Completion = ([NWSAlert]?, NWSError?) -> ()
#if DEBUG
public let updateInterval = TimeInterval(2 * 60) // 2 minutes
#else
public let updateInterval = TimeInterval(15 * 60) // 15 minutes
#endif
#if DEBUG
public var useStaging = true
#else
public var useStaging = false
#endif
private let baseUrlProduction = "https://nwsalert.onelouder.com"
private let baseUrlStaging = "https://sta-nwsalert.onelouder.com"
private let log = Logger(componentName: "NWSAlertsManager")
private var locationsBeingUpdated = Set<Location>()
private var baseUrl: String {
useStaging ? baseUrlStaging : baseUrlProduction
}
/// This queue is needed to synchronize access to locationsBeingUpdated and the alerts list. Also, to make logging more clear.
private let internalQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = "WdtWeatherSource Queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
private lazy var currentEventsDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
return dateFormatter
}()
public var alerts = [NWSAlert]()
public func fetchActiveAlerts(for location: Location, completion: @escaping NWSAlertsManager.Completion) {
internalQueue.addOperation { [weak self] in
let extendedCompletion: NWSAlertsManager.Completion = { [weak self] (updatedLocation, error) in
self?.internalQueue.addOperation {
completion(updatedLocation, error)
self?.locationsBeingUpdated.remove(location)
}
}
self?.fetchActiveAlertsInternal(for: location, completion: extendedCompletion)
}
}
private func fetchActiveAlertsInternal(for location: Location, completion: @escaping NWSAlertsManager.Completion) {
guard !locationsBeingUpdated.contains(location) else {
completion(nil, .alreadyBeingUpdated)
return
}
locationsBeingUpdated.insert(location)
log.info("Start update for \(location)")
var params: [String: String] = [
"format": "json"
]
if let fips_code = location.fipsCode {
params["weather_id"] = fips_code
}
else if let coordinates = location.coordinates{
params["geo"] = String(format: "%.5f,%.5f", coordinates.latitude, coordinates.longitude)
}
else {
completion(nil, .insufficientLocationInfo)
return
}
params["echoCity"] = location.cityName
guard var urlComponents = URLComponents(string: self.baseUrl.appending("/current_events")) else {
log.error("Couldn't create URLComponents from \(self.baseUrl)")
completion(nil, .badUrl)
fatalError("Should never happen. Couldn't create URL components from \(self.baseUrl). This URL has to always be correct.") // Should never happen, but a lot of stuff that should never happen happens from time to time, so let's at least handle it gracefully in prod.
return
}
urlComponents.queryItems = params.map { URLQueryItem(name: $0, value: $1) }
guard let url = urlComponents.url else {
log.error("Couldn't create URL with params: \(params)")
completion(nil, .badUrl)
return
}
let urlSession = URLSession.shared
let dataTask = urlSession.dataTask(with: url) { [weak self] (data, response, error) in
guard let self = self else { return }
guard let data = data else {
completion(nil, .networkError(error))
return
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(self.currentEventsDateFormatter)
do {
let response = try decoder.decode(NWSCurrentEventsReponse.self, from: data)
guard let events = response.events?.events else {
completion(nil, .badServerResponse(nil))
return
}
let eventsWithLocation: [NWSAlert] = events.map { (alert) -> NWSAlert in
var updated: NWSAlert = alert
updated.city = location.cityName ?? ""
return updated
}
self.merge(alerts: eventsWithLocation)
completion(eventsWithLocation, nil)
}
catch {
completion(nil, .badServerResponse(error))
}
}
dataTask.resume()
}
func merge(alerts newAlerts: [NWSAlert]) {
//TODO: optimize
var resultingSet = Set<NWSAlert>()
for alert in self.alerts {
if !alert.expired {
resultingSet.insert(alert)
}
}
for alert in newAlerts {
if !alert.expired {
resultingSet.insert(alert)
}
}
// TODO: maybe we need a more sophisticated logic for sorting: take severity into account, for instance.
let resultingAlerts = resultingSet.sorted(by: { $1.expires > $0.expires })
// sync, because we don't want another merge to strat before assignment to self.alerts completes.
// A deadlock shouldn't happen unless main thread waits on internalQueue, which it doesn't.
DispatchQueue.main.sync {
self.alerts = resultingAlerts
}
}
}
...@@ -68,7 +68,10 @@ public class WdtWeatherSource: WeatherSource { ...@@ -68,7 +68,10 @@ public class WdtWeatherSource: WeatherSource {
} }
guard var urlComponents = URLComponents(string: urlToUse) else { guard var urlComponents = URLComponents(string: urlToUse) else {
log.error("Couldn't create URLComponents from \(urlToUse)")
assertionFailure("Should never happen. The URL should be correct.") assertionFailure("Should never happen. The URL should be correct.")
// Should never happen, but a lot of stuff that should never happen happens from time to time, so let's at least handle it gracefully in prod.
completion(nil, WdtWeatherSourceError.badUrl)
return return
} }
...@@ -93,6 +96,7 @@ public class WdtWeatherSource: WeatherSource { ...@@ -93,6 +96,7 @@ public class WdtWeatherSource: WeatherSource {
urlComponents.queryItems = queryParameters.map { URLQueryItem(name: $0, value: $1) } urlComponents.queryItems = queryParameters.map { URLQueryItem(name: $0, value: $1) }
guard let url = urlComponents.url else { guard let url = urlComponents.url else {
log.error("Couldn't create URL with params: \(queryParameters)")
completion(nil, WdtWeatherSourceError.badUrl) completion(nil, WdtWeatherSourceError.badUrl)
return return
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment