Commit 31b6a632 by Dmitry Stepanets

[IOS-172]: Finished minutely UI & user iteractions. Merged with release 5.4

parent 40406fb5
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
87D815AC2636D61D0015A6D1 /* NWSAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D815AB2636D61D0015A6D1 /* NWSAlertViewModel.swift */; }; 87D815AC2636D61D0015A6D1 /* NWSAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D815AB2636D61D0015A6D1 /* NWSAlertViewModel.swift */; };
90766436021777888F434DCB /* Pods_OneWeatherWidgetExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C588E8BC9935BC4408CE6E27 /* Pods_OneWeatherWidgetExtension.framework */; }; 90766436021777888F434DCB /* Pods_OneWeatherWidgetExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C588E8BC9935BC4408CE6E27 /* Pods_OneWeatherWidgetExtension.framework */; };
9FAD89D1BEBA0FEB5F50BE73 /* Pods_1Weather.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BDC55F44AA0B2FA48788F0E /* Pods_1Weather.framework */; }; 9FAD89D1BEBA0FEB5F50BE73 /* Pods_1Weather.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BDC55F44AA0B2FA48788F0E /* Pods_1Weather.framework */; };
CD1000AE26D92A8F002D6C61 /* Array+Unique.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1000AD26D92A8F002D6C61 /* Array+Unique.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 */; };
CD1237F4255D889F00C98139 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1237F3255D889F00C98139 /* GradientView.swift */; }; CD1237F4255D889F00C98139 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1237F3255D889F00C98139 /* GradientView.swift */; };
...@@ -174,6 +175,8 @@ ...@@ -174,6 +175,8 @@
CDAC9B8526319B0500AC1BF4 /* MapTimeControlItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAC9B8426319B0500AC1BF4 /* MapTimeControlItem.swift */; }; CDAC9B8526319B0500AC1BF4 /* MapTimeControlItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAC9B8426319B0500AC1BF4 /* MapTimeControlItem.swift */; };
CDAD97B1262042B2007FCFB1 /* MapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAD97B0262042B2007FCFB1 /* MapButton.swift */; }; CDAD97B1262042B2007FCFB1 /* MapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAD97B0262042B2007FCFB1 /* MapButton.swift */; };
CDAD97B426207D14007FCFB1 /* MapTimeControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAD97B326207D14007FCFB1 /* MapTimeControlView.swift */; }; CDAD97B426207D14007FCFB1 /* MapTimeControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAD97B326207D14007FCFB1 /* MapTimeControlView.swift */; };
CDADBBA426D64948006E4565 /* MinutelyForecastCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDADBBA326D64948006E4565 /* MinutelyForecastCell.swift */; };
CDADBBA726D6552A006E4565 /* MinutelyForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDADBBA626D6552A006E4565 /* MinutelyForecastView.swift */; };
CDB0D4CA2670CAD00081C773 /* ShortsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB0D4C92670CAD00081C773 /* ShortsCollectionViewCell.swift */; }; CDB0D4CA2670CAD00081C773 /* ShortsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB0D4C92670CAD00081C773 /* ShortsCollectionViewCell.swift */; };
CDB0D4CC2670D12F0081C773 /* TodayShortsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB0D4CB2670D12F0081C773 /* TodayShortsCell.swift */; }; CDB0D4CC2670D12F0081C773 /* TodayShortsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB0D4CB2670D12F0081C773 /* TodayShortsCell.swift */; };
CDB0D4CE2670DABF0081C773 /* UIImage+AverageColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB0D4CD2670DABF0081C773 /* UIImage+AverageColor.swift */; }; CDB0D4CE2670DABF0081C773 /* UIImage+AverageColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB0D4CD2670DABF0081C773 /* UIImage+AverageColor.swift */; };
...@@ -192,6 +195,7 @@ ...@@ -192,6 +195,7 @@
CDD75F0D25DE68B10099ACDB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CDD75F0F25DE68B10099ACDB /* Localizable.strings */; }; CDD75F0D25DE68B10099ACDB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CDD75F0F25DE68B10099ACDB /* Localizable.strings */; };
CDDCD50726809F6D00E089AD /* ShortsSwipeHelperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDCD50626809F6D00E089AD /* ShortsSwipeHelperView.swift */; }; CDDCD50726809F6D00E089AD /* ShortsSwipeHelperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDCD50626809F6D00E089AD /* ShortsSwipeHelperView.swift */; };
CDDCD5092680C18B00E089AD /* ShortsUnreadNudgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDCD5082680C18B00E089AD /* ShortsUnreadNudgeView.swift */; }; CDDCD5092680C18B00E089AD /* ShortsUnreadNudgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDCD5082680C18B00E089AD /* ShortsUnreadNudgeView.swift */; };
CDDE2E9526D8DCAF00B32C05 /* MinutelyForecastDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDE2E9426D8DCAF00B32C05 /* MinutelyForecastDetailsView.swift */; };
CDDE8D7C262EED3C00267931 /* MapLegendSevereView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDE8D7B262EED3C00267931 /* MapLegendSevereView.swift */; }; CDDE8D7C262EED3C00267931 /* MapLegendSevereView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDE8D7B262EED3C00267931 /* MapLegendSevereView.swift */; };
CDDE8D7F262EED4D00267931 /* MapLegendWeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDE8D7E262EED4D00267931 /* MapLegendWeatherView.swift */; }; CDDE8D7F262EED4D00267931 /* MapLegendWeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDE8D7E262EED4D00267931 /* MapLegendWeatherView.swift */; };
CDE18DCD25D1666700C80ED9 /* ForecastCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE18DCC25D1666700C80ED9 /* ForecastCoordinator.swift */; }; CDE18DCD25D1666700C80ED9 /* ForecastCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE18DCC25D1666700C80ED9 /* ForecastCoordinator.swift */; };
...@@ -200,6 +204,8 @@ ...@@ -200,6 +204,8 @@
CDE2BF222609D4250085C930 /* ForecastWindSpeedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE2BF212609D4250085C930 /* ForecastWindSpeedCell.swift */; }; CDE2BF222609D4250085C930 /* ForecastWindSpeedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE2BF212609D4250085C930 /* ForecastWindSpeedCell.swift */; };
CDE2BF252609D9140085C930 /* ForecastWindButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE2BF242609D9140085C930 /* ForecastWindButton.swift */; }; CDE2BF252609D9140085C930 /* ForecastWindButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE2BF242609D9140085C930 /* ForecastWindButton.swift */; };
CDEE8AD725DA882200C289DE /* ForecastPeriodButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEE8AD625DA882200C289DE /* ForecastPeriodButton.swift */; }; CDEE8AD725DA882200C289DE /* ForecastPeriodButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEE8AD625DA882200C289DE /* ForecastPeriodButton.swift */; };
CDF079FE26D501BD00E797D9 /* BlendMinutelySource.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDF079FD26D501BD00E797D9 /* BlendMinutelySource.framework */; };
CDF079FF26D501BD00E797D9 /* BlendMinutelySource.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CDF079FD26D501BD00E797D9 /* BlendMinutelySource.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CDF63D29266779D8003DE569 /* AdLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF63D28266779D8003DE569 /* AdLogger.swift */; }; CDF63D29266779D8003DE569 /* AdLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF63D28266779D8003DE569 /* AdLogger.swift */; };
CDF6E87726A8329D004A9DBD /* WindWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF6E87626A8329D004A9DBD /* WindWidget.swift */; }; CDF6E87726A8329D004A9DBD /* WindWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF6E87626A8329D004A9DBD /* WindWidget.swift */; };
CDF8F12A262089A200DB384A /* MapTimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF8F129262089A200DB384A /* MapTimeView.swift */; }; CDF8F12A262089A200DB384A /* MapTimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF8F129262089A200DB384A /* MapTimeView.swift */; };
...@@ -297,6 +303,7 @@ ...@@ -297,6 +303,7 @@
CDFE45BD26566EF50021A29F /* WDTWeatherSource.framework in Embed Frameworks */, CDFE45BD26566EF50021A29F /* WDTWeatherSource.framework in Embed Frameworks */,
CD427D28266F856700B4350A /* InMobiShortsSource.framework in Embed Frameworks */, CD427D28266F856700B4350A /* InMobiShortsSource.framework in Embed Frameworks */,
CEEF4101265E47FF00425D8F /* BlendFIPSSource.framework in Embed Frameworks */, CEEF4101265E47FF00425D8F /* BlendFIPSSource.framework in Embed Frameworks */,
CDF079FF26D501BD00E797D9 /* BlendMinutelySource.framework in Embed Frameworks */,
CE13B97C2626FB11007CBD4D /* PSMLocationSDK.xcframework in Embed Frameworks */, CE13B97C2626FB11007CBD4D /* PSMLocationSDK.xcframework in Embed Frameworks */,
CE30E3802668FBE3006DF5CD /* OneWeatherAnalytics.framework in Embed Frameworks */, CE30E3802668FBE3006DF5CD /* OneWeatherAnalytics.framework in Embed Frameworks */,
CD615F7F265523BD00B717DB /* OneWeatherCore.framework in Embed Frameworks */, CD615F7F265523BD00B717DB /* OneWeatherCore.framework in Embed Frameworks */,
...@@ -336,6 +343,7 @@ ...@@ -336,6 +343,7 @@
87D815AB2636D61D0015A6D1 /* NWSAlertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSAlertViewModel.swift; sourceTree = "<group>"; }; 87D815AB2636D61D0015A6D1 /* NWSAlertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSAlertViewModel.swift; sourceTree = "<group>"; };
C588E8BC9935BC4408CE6E27 /* Pods_OneWeatherWidgetExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OneWeatherWidgetExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C588E8BC9935BC4408CE6E27 /* Pods_OneWeatherWidgetExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OneWeatherWidgetExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
CD1000AD26D92A8F002D6C61 /* Array+Unique.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Unique.swift"; 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>"; };
CD1237CB255D5C5C00C98139 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; CD1237CB255D5C5C00C98139 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
...@@ -484,6 +492,8 @@ ...@@ -484,6 +492,8 @@
CDAC9B8426319B0500AC1BF4 /* MapTimeControlItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTimeControlItem.swift; sourceTree = "<group>"; }; CDAC9B8426319B0500AC1BF4 /* MapTimeControlItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTimeControlItem.swift; sourceTree = "<group>"; };
CDAD97B0262042B2007FCFB1 /* MapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapButton.swift; sourceTree = "<group>"; }; CDAD97B0262042B2007FCFB1 /* MapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapButton.swift; sourceTree = "<group>"; };
CDAD97B326207D14007FCFB1 /* MapTimeControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTimeControlView.swift; sourceTree = "<group>"; }; CDAD97B326207D14007FCFB1 /* MapTimeControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTimeControlView.swift; sourceTree = "<group>"; };
CDADBBA326D64948006E4565 /* MinutelyForecastCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinutelyForecastCell.swift; sourceTree = "<group>"; };
CDADBBA626D6552A006E4565 /* MinutelyForecastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinutelyForecastView.swift; sourceTree = "<group>"; };
CDB0D4C92670CAD00081C773 /* ShortsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortsCollectionViewCell.swift; sourceTree = "<group>"; }; CDB0D4C92670CAD00081C773 /* ShortsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortsCollectionViewCell.swift; sourceTree = "<group>"; };
CDB0D4CB2670D12F0081C773 /* TodayShortsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayShortsCell.swift; sourceTree = "<group>"; }; CDB0D4CB2670D12F0081C773 /* TodayShortsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayShortsCell.swift; sourceTree = "<group>"; };
CDB0D4CD2670DABF0081C773 /* UIImage+AverageColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+AverageColor.swift"; sourceTree = "<group>"; }; CDB0D4CD2670DABF0081C773 /* UIImage+AverageColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+AverageColor.swift"; sourceTree = "<group>"; };
...@@ -502,6 +512,7 @@ ...@@ -502,6 +512,7 @@
CDD75F0E25DE68B10099ACDB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; CDD75F0E25DE68B10099ACDB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
CDDCD50626809F6D00E089AD /* ShortsSwipeHelperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortsSwipeHelperView.swift; sourceTree = "<group>"; }; CDDCD50626809F6D00E089AD /* ShortsSwipeHelperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortsSwipeHelperView.swift; sourceTree = "<group>"; };
CDDCD5082680C18B00E089AD /* ShortsUnreadNudgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortsUnreadNudgeView.swift; sourceTree = "<group>"; }; CDDCD5082680C18B00E089AD /* ShortsUnreadNudgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortsUnreadNudgeView.swift; sourceTree = "<group>"; };
CDDE2E9426D8DCAF00B32C05 /* MinutelyForecastDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinutelyForecastDetailsView.swift; sourceTree = "<group>"; };
CDDE8D7B262EED3C00267931 /* MapLegendSevereView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLegendSevereView.swift; sourceTree = "<group>"; }; CDDE8D7B262EED3C00267931 /* MapLegendSevereView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLegendSevereView.swift; sourceTree = "<group>"; };
CDDE8D7E262EED4D00267931 /* MapLegendWeatherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLegendWeatherView.swift; sourceTree = "<group>"; }; CDDE8D7E262EED4D00267931 /* MapLegendWeatherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLegendWeatherView.swift; sourceTree = "<group>"; };
CDE18DCC25D1666700C80ED9 /* ForecastCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastCoordinator.swift; sourceTree = "<group>"; }; CDE18DCC25D1666700C80ED9 /* ForecastCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastCoordinator.swift; sourceTree = "<group>"; };
...@@ -511,6 +522,7 @@ ...@@ -511,6 +522,7 @@
CDE2BF242609D9140085C930 /* ForecastWindButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastWindButton.swift; sourceTree = "<group>"; }; CDE2BF242609D9140085C930 /* ForecastWindButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastWindButton.swift; sourceTree = "<group>"; };
CDEE8AD625DA882200C289DE /* ForecastPeriodButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastPeriodButton.swift; sourceTree = "<group>"; }; CDEE8AD625DA882200C289DE /* ForecastPeriodButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastPeriodButton.swift; sourceTree = "<group>"; };
CDEF70E2266E10B600BA40D6 /* OneWeatherCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CDEF70E2266E10B600BA40D6 /* OneWeatherCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CDF079FD26D501BD00E797D9 /* BlendMinutelySource.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BlendMinutelySource.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CDF63D28266779D8003DE569 /* AdLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdLogger.swift; sourceTree = "<group>"; }; CDF63D28266779D8003DE569 /* AdLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdLogger.swift; sourceTree = "<group>"; };
CDF6E87626A8329D004A9DBD /* WindWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindWidget.swift; sourceTree = "<group>"; }; CDF6E87626A8329D004A9DBD /* WindWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindWidget.swift; sourceTree = "<group>"; };
CDF8F129262089A200DB384A /* MapTimeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTimeView.swift; sourceTree = "<group>"; }; CDF8F129262089A200DB384A /* MapTimeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTimeView.swift; sourceTree = "<group>"; };
...@@ -594,6 +606,7 @@ ...@@ -594,6 +606,7 @@
CD3884552657BA8B0070FD6F /* CoreDataStorage.framework in Frameworks */, CD3884552657BA8B0070FD6F /* CoreDataStorage.framework in Frameworks */,
CD3884832657BBCC0070FD6F /* DelayedSaveStorage.framework in Frameworks */, CD3884832657BBCC0070FD6F /* DelayedSaveStorage.framework in Frameworks */,
CEEF4100265E47FF00425D8F /* BlendFIPSSource.framework in Frameworks */, CEEF4100265E47FF00425D8F /* BlendFIPSSource.framework in Frameworks */,
CDF079FE26D501BD00E797D9 /* BlendMinutelySource.framework in Frameworks */,
CD615F7E265523BD00B717DB /* OneWeatherCore.framework in Frameworks */, CD615F7E265523BD00B717DB /* OneWeatherCore.framework in Frameworks */,
CE30E37F2668FBE3006DF5CD /* OneWeatherAnalytics.framework in Frameworks */, CE30E37F2668FBE3006DF5CD /* OneWeatherAnalytics.framework in Frameworks */,
9FAD89D1BEBA0FEB5F50BE73 /* Pods_1Weather.framework in Frameworks */, 9FAD89D1BEBA0FEB5F50BE73 /* Pods_1Weather.framework in Frameworks */,
...@@ -924,6 +937,7 @@ ...@@ -924,6 +937,7 @@
CD7D318A268F36AF000D01FA /* UIApplication+Version.swift */, CD7D318A268F36AF000D01FA /* UIApplication+Version.swift */,
CD85797226721DD400CC4CDA /* UIColor+Highlight.swift */, CD85797226721DD400CC4CDA /* UIColor+Highlight.swift */,
CDB0D4CD2670DABF0081C773 /* UIImage+AverageColor.swift */, CDB0D4CD2670DABF0081C773 /* UIImage+AverageColor.swift */,
CD1000AD26D92A8F002D6C61 /* Array+Unique.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -1192,9 +1206,30 @@ ...@@ -1192,9 +1206,30 @@
path = MapTimeControl; path = MapTimeControl;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CDADBBA226D64921006E4565 /* MinutelyForecastCell */ = {
isa = PBXGroup;
children = (
CDADBBA326D64948006E4565 /* MinutelyForecastCell.swift */,
);
path = MinutelyForecastCell;
sourceTree = "<group>";
};
CDADBBA526D654FA006E4565 /* SharedViews */ = {
isa = PBXGroup;
children = (
CDDE2E9326D8DC8900B32C05 /* MinutelyForecastView */,
CD0A2CC325FA5857006148A4 /* ForecastTimePeriod */,
CD1237F3255D889F00C98139 /* GradientView.swift */,
CD86246B25E6826A0097F3FB /* InnerShadowLayer.swift */,
CD32CDFE260B2E5400235081 /* ForecastDescriptionView.swift */,
);
path = SharedViews;
sourceTree = "<group>";
};
CDC0C7AE261310DB0030607A /* SharedCells */ = { CDC0C7AE261310DB0030607A /* SharedCells */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CDADBBA226D64921006E4565 /* MinutelyForecastCell */,
CEEB354A266F687300E16F90 /* AdCells */, CEEB354A266F687300E16F90 /* AdCells */,
CD86246325E66E6B0097F3FB /* PrecipCell */, CD86246325E66E6B0097F3FB /* PrecipCell */,
CD86245B25E646000097F3FB /* SunPhaseCell */, CD86245B25E646000097F3FB /* SunPhaseCell */,
...@@ -1242,6 +1277,7 @@ ...@@ -1242,6 +1277,7 @@
CDD0F1DC2572400200CF5017 /* UI */ = { CDD0F1DC2572400200CF5017 /* UI */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CDADBBA526D654FA006E4565 /* SharedViews */,
CDC0C7AE261310DB0030607A /* SharedCells */, CDC0C7AE261310DB0030607A /* SharedCells */,
CD15DB3B25DA6C2800024727 /* Controls */, CD15DB3B25DA6C2800024727 /* Controls */,
CD6B3039257267FB004B34B3 /* Buttons */, CD6B3039257267FB004B34B3 /* Buttons */,
...@@ -1279,14 +1315,10 @@ ...@@ -1279,14 +1315,10 @@
CDD0F1F025725BD700CF5017 /* Helpers */ = { CDD0F1F025725BD700CF5017 /* Helpers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CD0A2CC325FA5857006148A4 /* ForecastTimePeriod */,
CD6B304125726ABE004B34B3 /* Themes */, CD6B304125726ABE004B34B3 /* Themes */,
CD1237F3255D889F00C98139 /* GradientView.swift */,
CDD0F1E72572429E00CF5017 /* AppFont.swift */, CDD0F1E72572429E00CF5017 /* AppFont.swift */,
CDD0F1ED25725BCF00CF5017 /* ThemeManager.swift */, CDD0F1ED25725BCF00CF5017 /* ThemeManager.swift */,
CD50555E26983C14006776AB /* CubicCurveAlgorithm.swift */, CD50555E26983C14006776AB /* CubicCurveAlgorithm.swift */,
CD86246B25E6826A0097F3FB /* InnerShadowLayer.swift */,
CD32CDFE260B2E5400235081 /* ForecastDescriptionView.swift */,
); );
path = Helpers; path = Helpers;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -1300,6 +1332,15 @@ ...@@ -1300,6 +1332,15 @@
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CDDE2E9326D8DC8900B32C05 /* MinutelyForecastView */ = {
isa = PBXGroup;
children = (
CDADBBA626D6552A006E4565 /* MinutelyForecastView.swift */,
CDDE2E9426D8DCAF00B32C05 /* MinutelyForecastDetailsView.swift */,
);
path = MinutelyForecastView;
sourceTree = "<group>";
};
CDE18DCF25D166DD00C80ED9 /* Forecast */ = { CDE18DCF25D166DD00C80ED9 /* Forecast */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
...@@ -1533,6 +1574,7 @@ ...@@ -1533,6 +1574,7 @@
DBFD169AA2AA6A3CB5B68BB5 /* Frameworks */ = { DBFD169AA2AA6A3CB5B68BB5 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CDF079FD26D501BD00E797D9 /* BlendMinutelySource.framework */,
CD5909CF26A59AAA00448579 /* OneWeatherUI.framework */, CD5909CF26A59AAA00448579 /* OneWeatherUI.framework */,
CD5181BF269EEB61008E6B04 /* CoreLocation.framework */, CD5181BF269EEB61008E6B04 /* CoreLocation.framework */,
CDEF70E2266E10B600BA40D6 /* OneWeatherCore.framework */, CDEF70E2266E10B600BA40D6 /* OneWeatherCore.framework */,
...@@ -1939,6 +1981,7 @@ ...@@ -1939,6 +1981,7 @@
CE6E410626EBA3EB009829AE /* SubscriptionOverviewViewController.swift in Sources */, CE6E410626EBA3EB009829AE /* SubscriptionOverviewViewController.swift in Sources */,
CDB0D4CA2670CAD00081C773 /* ShortsCollectionViewCell.swift in Sources */, CDB0D4CA2670CAD00081C773 /* ShortsCollectionViewCell.swift in Sources */,
CDDCD5092680C18B00E089AD /* ShortsUnreadNudgeView.swift in Sources */, CDDCD5092680C18B00E089AD /* ShortsUnreadNudgeView.swift in Sources */,
CDADBBA726D6552A006E4565 /* MinutelyForecastView.swift in Sources */,
CE13B818262480B3007CBD4D /* A9BidObject.swift in Sources */, CE13B818262480B3007CBD4D /* A9BidObject.swift in Sources */,
CD85796A2671FA8100CC4CDA /* ShortsCoordinator.swift in Sources */, CD85796A2671FA8100CC4CDA /* ShortsCoordinator.swift in Sources */,
CDB0D4CE2670DABF0081C773 /* UIImage+AverageColor.swift in Sources */, CDB0D4CE2670DABF0081C773 /* UIImage+AverageColor.swift in Sources */,
...@@ -1990,6 +2033,7 @@ ...@@ -1990,6 +2033,7 @@
CD593BCF2608A50900C93428 /* ForecastHourlyCell.swift in Sources */, CD593BCF2608A50900C93428 /* ForecastHourlyCell.swift in Sources */,
CDB0D4CC2670D12F0081C773 /* TodayShortsCell.swift in Sources */, CDB0D4CC2670D12F0081C773 /* TodayShortsCell.swift in Sources */,
CD1DDD332602305200AC62B2 /* ForecastInfoCell.swift in Sources */, CD1DDD332602305200AC62B2 /* ForecastInfoCell.swift in Sources */,
CDDE2E9526D8DCAF00B32C05 /* MinutelyForecastDetailsView.swift in Sources */,
CD8579792672214700CC4CDA /* UITableView+HeaderSize.swift in Sources */, CD8579792672214700CC4CDA /* UITableView+HeaderSize.swift in Sources */,
CDEE8AD725DA882200C289DE /* ForecastPeriodButton.swift in Sources */, CDEE8AD725DA882200C289DE /* ForecastPeriodButton.swift in Sources */,
CE6E411426EBC0E9009829AE /* LocalizationChangeObserver.swift in Sources */, CE6E411426EBC0E9009829AE /* LocalizationChangeObserver.swift in Sources */,
...@@ -2026,6 +2070,7 @@ ...@@ -2026,6 +2070,7 @@
CD67617026259D220079D273 /* RadarMapLayersController.swift in Sources */, CD67617026259D220079D273 /* RadarMapLayersController.swift in Sources */,
CEC8FBAF2639756A0001A6BF /* OnboardingViewController.swift in Sources */, CEC8FBAF2639756A0001A6BF /* OnboardingViewController.swift in Sources */,
CD866A65260F642600E96A5C /* SettingsDetailsViewController.swift in Sources */, CD866A65260F642600E96A5C /* SettingsDetailsViewController.swift in Sources */,
CDADBBA426D64948006E4565 /* MinutelyForecastCell.swift in Sources */,
CD647D0225ED07D60034578B /* TodayViewModel.swift in Sources */, CD647D0225ED07D60034578B /* TodayViewModel.swift in Sources */,
CD593BD32608BC3F00C93428 /* ForecastDayCell.swift in Sources */, CD593BD32608BC3F00C93428 /* ForecastDayCell.swift in Sources */,
CD8B60AB263819400055CB3F /* NWSAlertCellFactory.swift in Sources */, CD8B60AB263819400055CB3F /* NWSAlertCellFactory.swift in Sources */,
...@@ -2074,6 +2119,8 @@ ...@@ -2074,6 +2119,8 @@
CE578FE625FB415F00E8B85D /* LocationViewController.swift in Sources */, CE578FE625FB415F00E8B85D /* LocationViewController.swift in Sources */,
CD86246525E66E8A0097F3FB /* PrecipitationCell.swift in Sources */, CD86246525E66E8A0097F3FB /* PrecipitationCell.swift in Sources */,
CE13B821262480B3007CBD4D /* Scheduler.swift in Sources */, CE13B821262480B3007CBD4D /* Scheduler.swift in Sources */,
CE13B81E262480B3007CBD4D /* AdCacheManager.swift in Sources */,
CD1000AE26D92A8F002D6C61 /* Array+Unique.swift in Sources */,
CEBAC1C82638240800A89681 /* DeeplinksRouter.swift in Sources */, CEBAC1C82638240800A89681 /* DeeplinksRouter.swift in Sources */,
CD3F6E6925FA59D4002DB99B /* ForecastDetailPeriodButton.swift in Sources */, CD3F6E6925FA59D4002DB99B /* ForecastDetailPeriodButton.swift in Sources */,
CD37D405260DFFDD002669D6 /* CellFactory.swift in Sources */, CD37D405260DFFDD002669D6 /* CellFactory.swift in Sources */,
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
</Testables> </Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Release" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"
...@@ -50,6 +50,9 @@ ...@@ -50,6 +50,9 @@
ReferencedContainer = "container:1Weather.xcodeproj"> ReferencedContainer = "container:1Weather.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<StoreKitConfigurationFileReference
identifier = "../1Weather/InApps/Configuration.storekit">
</StoreKitConfigurationFileReference>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
......
...@@ -17,6 +17,9 @@ ...@@ -17,6 +17,9 @@
location = "group:BlendHealthSource/BlendHealthSource.xcodeproj"> location = "group:BlendHealthSource/BlendHealthSource.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:BlendMinutelySource/BlendMinutelySource.xcodeproj">
</FileRef>
<FileRef
location = "group:CoreDataStorage/CoreDataStorage.xcodeproj"> location = "group:CoreDataStorage/CoreDataStorage.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
......
...@@ -21,6 +21,7 @@ import OneWeatherUI ...@@ -21,6 +21,7 @@ import OneWeatherUI
import WDTWeatherSource import WDTWeatherSource
import BlendHealthSource import BlendHealthSource
import BlendMinutelySource
import BlendFIPSSource import BlendFIPSSource
import CoreDataStorage import CoreDataStorage
import DelayedSaveStorage import DelayedSaveStorage
...@@ -65,6 +66,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -65,6 +66,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
//TODO: introduce dependency management (dependency injection engine coupled with a factory or something of the sort). //TODO: introduce dependency management (dependency injection engine coupled with a factory or something of the sort).
LocationManager.shared = LocationManager(weatherUpdateSource: WdtWeatherSource(), LocationManager.shared = LocationManager(weatherUpdateSource: WdtWeatherSource(),
healthSource: BlendHealthSource(), healthSource: BlendHealthSource(),
minutelyForecastSource: BlendMinutelySource(),
nwsAlertsManager: NWSAlertsManager(), nwsAlertsManager: NWSAlertsManager(),
fipsSource: BlendFIPSSource(), fipsSource: BlendFIPSSource(),
pushNotificationsManager: PushNotificationsManager.shared, pushNotificationsManager: PushNotificationsManager.shared,
......
//
// Array+Unique.swift
// 1Weather
//
// Created by Dmitry Stepanets on 27.08.2021.
//
import Foundation
extension Sequence where Iterator.Element: Hashable {
func unique() -> [Iterator.Element] {
var seen: Set<Iterator.Element> = []
return filter { seen.insert($0).inserted }
}
}
...@@ -10,6 +10,7 @@ import UIKit ...@@ -10,6 +10,7 @@ import UIKit
enum TimePeriod: Int { enum TimePeriod: Int {
case daily = 0 case daily = 0
case hourly = 1 case hourly = 1
case minutely = 2
} }
class ForecastTimePeriodControl: UISegmentedControl { class ForecastTimePeriodControl: UISegmentedControl {
...@@ -29,6 +30,27 @@ class ForecastTimePeriodControl: UISegmentedControl { ...@@ -29,6 +30,27 @@ class ForecastTimePeriodControl: UISegmentedControl {
updateUI() updateUI()
} }
public func set(items: [String]?) {
defer {
updateUI()
layoutSubviews()
}
guard
let itemsToAdd = items,
!itemsToAdd.isEmpty
else {
self.removeAllSegments()
return
}
self.removeAllSegments()
for (index, item) in itemsToAdd.enumerated() {
self.insertSegment(withTitle: item, at: index, animated: false)
}
selectedSegmentIndex = 0
}
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
......
//
// MinutelyForecastCell.swift
// 1Weather
//
// Created by Dmitry Stepanets on 25.08.2021.
//
import UIKit
class MinutelyForecastCell: UITableViewCell {
private let horizontalStackView = UIStackView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
...@@ -12,8 +12,11 @@ class PrecipitationCell: UITableViewCell { ...@@ -12,8 +12,11 @@ class PrecipitationCell: UITableViewCell {
//Private //Private
private let headingLabel = UILabel() private let headingLabel = UILabel()
private let headingButton = ArrowButton() private let headingButton = ArrowButton()
private let minutelyForecastView = MinutelyForecastView()
private let featureAvailability = FeatureAvailabilityManager.shared
private let scrollView = UIScrollView() private let scrollView = UIScrollView()
private let stackView = UIStackView() private let stackView = UIStackView()
private let periodSegmentedControl = ForecastTimePeriodControl(items: nil)
private let descriptionView = ForecastDescriptionView(lightStyleBackgroundColor: UIColor(hex: 0xd9ebfe), private let descriptionView = ForecastDescriptionView(lightStyleBackgroundColor: UIColor(hex: 0xd9ebfe),
gradientColors: [UIColor(hex: 0x44a4ff).withAlphaComponent(0.65).cgColor, gradientColors: [UIColor(hex: 0x44a4ff).withAlphaComponent(0.65).cgColor,
UIColor(hex: 0x73bbff).withAlphaComponent(0).cgColor]) UIColor(hex: 0x73bbff).withAlphaComponent(0).cgColor])
...@@ -23,6 +26,8 @@ class PrecipitationCell: UITableViewCell { ...@@ -23,6 +26,8 @@ class PrecipitationCell: UITableViewCell {
prepareCell() prepareCell()
prepareHeading() prepareHeading()
prepareSegmentControl()
prepareMinutelyForecastView()
prepareScrollView() prepareScrollView()
prepareStackView() prepareStackView()
prepareSummaryView() prepareSummaryView()
...@@ -32,9 +37,28 @@ class PrecipitationCell: UITableViewCell { ...@@ -32,9 +37,28 @@ class PrecipitationCell: UITableViewCell {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
public func configure(with dayily:[DailyWeather]) { public func configure(with dayily:[DailyWeather], location: Location) {
//TODO: Hide button for now //TODO: Hide button for now
headingButton.isHidden = true headingButton.isHidden = true
minutelyForecastView.configure(with: location, forecastType: .precipitation)
//Update segment control
if featureAvailability?.isAvailable(feature: .minutelyForecast) == true {
periodSegmentedControl.set(items: ["forecast.timePeriod.daily".localized,
"forecast.timePeriod.minutely".localized()])
periodSegmentedControl.isHidden = false
periodSegmentedControl.snp.updateConstraints { update in
update.height.equalTo(40)
}
}
else {
periodSegmentedControl.set(items: ["forecast.timePeriod.daily".localized])
periodSegmentedControl.isHidden = true
periodSegmentedControl.snp.updateConstraints { update in
update.height.equalTo(0)
}
}
if stackView.arrangedSubviews.count != dayily.count { if stackView.arrangedSubviews.count != dayily.count {
let diff = stackView.arrangedSubviews.count - dayily.count let diff = stackView.arrangedSubviews.count - dayily.count
...@@ -67,14 +91,35 @@ class PrecipitationCell: UITableViewCell { ...@@ -67,14 +91,35 @@ class PrecipitationCell: UITableViewCell {
precipButton.configure(with: dayily[index]) precipButton.configure(with: dayily[index])
} }
self.handleSegmentDidChange()
} }
public func configure(with hourly:[HourlyWeather]) { public func configure(with hourly:[HourlyWeather], location: Location) {
self.headingLabel.font = AppFont.SFPro.bold(size: 18) self.headingLabel.font = AppFont.SFPro.bold(size: 18)
self.headingButton.isHidden = true self.headingButton.isHidden = true
self.headingLabel.text = "precipitation.title".localized().capitalized self.headingLabel.text = "precipitation.title".localized().capitalized
self.headingLabel.textColor = ThemeManager.currentTheme.primaryTextColor self.headingLabel.textColor = ThemeManager.currentTheme.primaryTextColor
minutelyForecastView.configure(with: location, forecastType: .precipitation)
//Update segment control
if featureAvailability?.isAvailable(feature: .minutelyForecast) == true {
periodSegmentedControl.set(items: ["forecast.timePeriod.hourly".localized,
"forecast.timePeriod.minutely".localized()])
periodSegmentedControl.isHidden = false
periodSegmentedControl.snp.updateConstraints { update in
update.height.equalTo(40)
}
}
else {
periodSegmentedControl.set(items: ["forecast.timePeriod.hourly".localized])
periodSegmentedControl.isHidden = true
periodSegmentedControl.snp.updateConstraints { update in
update.height.equalTo(0)
}
}
if stackView.arrangedSubviews.count != hourly.count { if stackView.arrangedSubviews.count != hourly.count {
let diff = stackView.arrangedSubviews.count - hourly.count let diff = stackView.arrangedSubviews.count - hourly.count
for _ in 0..<abs(diff) { for _ in 0..<abs(diff) {
...@@ -106,6 +151,8 @@ class PrecipitationCell: UITableViewCell { ...@@ -106,6 +151,8 @@ class PrecipitationCell: UITableViewCell {
precipButton.configure(with: hourly[index]) precipButton.configure(with: hourly[index])
} }
handleSegmentDidChange()
} }
//Private //Private
...@@ -120,6 +167,17 @@ class PrecipitationCell: UITableViewCell { ...@@ -120,6 +167,17 @@ class PrecipitationCell: UITableViewCell {
} }
} }
} }
@objc private func handleSegmentDidChange() {
if self.periodSegmentedControl.selectedSegmentIndex == 0 {
scrollView.isHidden = false
minutelyForecastView.isHidden = true
}
else {
scrollView.isHidden = true
minutelyForecastView.isHidden = false
}
}
} }
//MARK:- Prepare //MARK:- Prepare
...@@ -152,6 +210,18 @@ private extension PrecipitationCell { ...@@ -152,6 +210,18 @@ private extension PrecipitationCell {
} }
} }
func prepareSegmentControl() {
periodSegmentedControl.selectedSegmentIndex = 0
periodSegmentedControl.addTarget(self, action: #selector(handleSegmentDidChange), for: .valueChanged)
contentView.addSubview(periodSegmentedControl)
periodSegmentedControl.snp.makeConstraints { (make) in
make.top.equalTo(headingLabel.snp.bottom).offset(18)
make.left.right.equalToSuperview().inset(16)
make.height.equalTo(40)
}
}
func prepareScrollView() { func prepareScrollView() {
scrollView.showsVerticalScrollIndicator = false scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false
...@@ -161,7 +231,19 @@ private extension PrecipitationCell { ...@@ -161,7 +231,19 @@ private extension PrecipitationCell {
scrollView.snp.makeConstraints { (make) in scrollView.snp.makeConstraints { (make) in
make.left.equalToSuperview() make.left.equalToSuperview()
make.right.equalToSuperview() make.right.equalToSuperview()
make.top.equalTo(headingLabel.snp.bottom).offset(18) make.top.equalTo(periodSegmentedControl.snp.bottom).offset(18)
make.height.equalTo(240)
}
}
func prepareMinutelyForecastView() {
minutelyForecastView.isHidden = true
contentView.addSubview(minutelyForecastView)
minutelyForecastView.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.top.equalTo(periodSegmentedControl.snp.bottom).offset(40).priority(.medium)
make.height.equalTo(240) make.height.equalTo(240)
} }
} }
......
//
// MinutelyForecastDetailsView.swift
// 1Weather
//
// Created by Dmitry Stepanets on 27.08.2021.
//
import UIKit
import OneWeatherCore
class MinutelyForecastDetailsView: UIView {
//Private
private let container = UIView()
private let gradient = CAGradientLayer()
private let tempLabel = UILabel()
private let timeLabel = UILabel()
private let forecastImage = UIImageView()
private let triangle = CAShapeLayer()
private let triangleView = UIView()
private let formatter: DateFormatter
init() {
self.formatter = DateFormatter()
self.formatter.dateFormat = "h:mm a"
super.init(frame: .zero)
prepareTriangle()
prepareView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
//Gradient
gradient.frame = container.bounds
//Triangle
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: triangleView.bounds.width / 2, y: triangleView.bounds.height))
path.addLine(to: CGPoint(x: triangleView.bounds.width, y: 0))
path.addLine(to: CGPoint(x: 0, y: 0))
triangle.frame = triangleView.bounds
triangle.path = path
}
func configure(valueStirng: String, date: Date?, weatherImage: UIImage?, timeZone: TimeZone, colors: [UIColor]) {
gradient.colors = colors.map{ $0.cgColor }
triangle.fillColor = colors.last?.cgColor
formatter.timeZone = timeZone
if let forecastDate = date {
timeLabel.text = formatter.string(from: forecastDate)
}
else {
timeLabel.text = "--"
}
tempLabel.text = valueStirng
forecastImage.image = weatherImage
}
}
private extension MinutelyForecastDetailsView {
func prepareView() {
addSubview(container)
gradient.startPoint = .init(x: 0.5, y: 0)
gradient.endPoint = .init(x: 0.5, y: 1)
gradient.cornerRadius = 8
container.layer.addSublayer(gradient)
timeLabel.font = AppFont.SFPro.light(size: 12)
timeLabel.textColor = .white
container.addSubview(timeLabel)
tempLabel.font = AppFont.SFPro.bold(size: 24)
tempLabel.textColor = .white
container.addSubview(tempLabel)
forecastImage.contentMode = .scaleAspectFit
forecastImage.clipsToBounds = true
container.addSubview(forecastImage)
let separator = UIView()
separator.backgroundColor = .white.withAlphaComponent(0.5)
container.addSubview(separator)
//Constraints
container.snp.makeConstraints { make in
make.left.top.right.equalToSuperview()
make.bottom.equalTo(triangleView.snp.top)
}
timeLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().inset(12)
}
separator.snp.makeConstraints { make in
make.width.equalTo(1)
make.height.equalToSuperview().multipliedBy(0.65)
make.left.equalToSuperview().inset(70)
make.centerY.equalToSuperview()
}
forecastImage.snp.makeConstraints { make in
make.width.height.equalTo(28)
make.centerY.equalToSuperview()
make.right.equalToSuperview().inset(8)
}
tempLabel.snp.makeConstraints { make in
make.right.equalTo(forecastImage.snp.left).offset(-2)
make.centerY.equalToSuperview()
}
}
func prepareTriangle() {
triangleView.layer.addSublayer(triangle)
addSubview(triangleView)
triangleView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview()
make.width.equalTo(12)
make.height.equalTo(10)
}
}
}
//
// MinutelyForecastView.swift
// 1Weather
//
// Created by Dmitry Stepanets on 25.08.2021.
//
import UIKit
import OneWeatherCore
import Accelerate
enum MinutelyForecastType {
case temperature
case precipitation
}
private let kTemperatureColors = [UIColor(hex: 0xff934f), UIColor(hex: 0xff414a)]
private let kPrecipitationColors = [UIColor(hex: 0x2d99ff), UIColor(hex: 0x8fc6fb)]
private class MinutelyLevelView: UIView {
private let gradient = CAGradientLayer()
init(forecastType: MinutelyForecastType) {
super.init(frame: .zero)
gradient.startPoint = .init(x: 0.5, y: 0)
gradient.endPoint = .init(x: 0.5, y: 1)
switch forecastType {
case .temperature:
gradient.colors = kTemperatureColors.compactMap{ $0.cgColor }
case .precipitation:
gradient.colors = kPrecipitationColors.compactMap{ $0.cgColor }
}
layer.addSublayer(gradient)
//Shadow
layer.shadowColor = UIColor(hex: 0xd8ddfa).withAlphaComponent(0.8).cgColor
layer.shadowOffset = .init(width: 0, height: 3)
layer.shadowRadius = 3
layer.shadowOpacity = interfaceStyle == .light ? 1 : 0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
gradient.cornerRadius = bounds.width / 2
gradient.frame = bounds
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: bounds.width / 2).cgPath
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
layer.shadowOpacity = interfaceStyle == .light ? 1 : 0
}
}
class MinutelyForecastView: UIView {
//Private
private let kLevelWidth = 3
private let detailsInfoView = MinutelyForecastDetailsView()
private let levelsStackView = UIStackView()
private let verticalStackView = UIStackView()
private let scrollView = UIScrollView()
private let centerDashline = CAShapeLayer()
private var levelsDashline = [CAShapeLayer]()
private let feedbackGenerator = UISelectionFeedbackGenerator()
private var levelsPositionXCache = [Int : CGFloat]()
private var weatherTypeCache = [Int : UIImage]()
private var lastSelectedLevelIndex = 0
private var minutelyForecast = [MinutelyItem]()
private var forecastType = MinutelyForecastType.temperature
private lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "h:mm a"
return formatter
}()
private var location: Location?
init() {
super.init(frame: .zero)
prepareDetailView()
prepareCenterDashLine()
prepareScrollView()
prepareVerticalStackView()
}
override func layoutSubviews() {
super.layoutSubviews()
let leftInset = detailsInfoView.frame.origin.x - scrollView.frame.origin.x + detailsInfoView.frame.width / 2
if scrollView.contentInset.left != leftInset {
scrollView.contentInset = .init(top: 0, left: leftInset, bottom: 0, right: leftInset + 20)
scrollView.setContentOffset(.init(x: -leftInset, y: 0), animated: false)
}
//Dash
let dashlinePath = CGMutablePath()
dashlinePath.move(to: .init(x: detailsInfoView.frame.origin.x + detailsInfoView.bounds.width / 2,
y: detailsInfoView.frame.origin.y + detailsInfoView.bounds.height))
dashlinePath.addLine(to: .init(x: detailsInfoView.frame.origin.x + detailsInfoView.bounds.width / 2,
y: verticalStackView.frame.origin.y + verticalStackView.bounds.height))
centerDashline.path = dashlinePath
//Levels dashline
for (index, levelDashShape) in levelsDashline.enumerated() {
let dashlinePath = CGMutablePath()
let levelView = verticalStackView.arrangedSubviews[index]
dashlinePath.move(to: .init(x: scrollView.frame.origin.x,
y: verticalStackView.frame.origin.y + levelView.frame.origin.y + levelView.frame.height / 2))
dashlinePath.addLine(to: .init(x: scrollView.frame.width + scrollView.frame.origin.x,
y: verticalStackView.frame.origin.y + levelView.frame.origin.y + levelView.frame.height / 2))
levelDashShape.path = dashlinePath
}
}
func configure(with location: Location, forecastType: MinutelyForecastType) {
self.location = location
self.forecastType = forecastType
self.dateFormatter.timeZone = location.timeZone
centerDashline.strokeColor = forecastType == .temperature ? kTemperatureColors.last?.cgColor
: kPrecipitationColors.last?.cgColor
prepareMinutelyItems()
if let firstMinutelyItem = minutelyForecast.first {
self.updateDetailsView(minutelyItem: firstMinutelyItem)
}
updateChart()
}
private func updateDetailsView(minutelyItem: MinutelyItem) {
switch forecastType {
case .temperature:
self.detailsInfoView.configure(valueStirng: minutelyItem.temp.shortString,
date: minutelyItem.time,
weatherImage: minutelyItem.weatherTypeImage,
timeZone: location?.timeZone ?? .current,
colors: kTemperatureColors)
case .precipitation:
self.detailsInfoView.configure(valueStirng: "\(Int(minutelyItem.precipitation * 100))%",
date: minutelyItem.time,
weatherImage: minutelyItem.weatherTypeImage,
timeZone: location?.timeZone ?? .current,
colors: kPrecipitationColors)
}
}
private func prepareMinutelyItems() {
minutelyForecast.removeAll()
guard
let location = self.location,
let forecastItems = location.minutely?.forecast
else {
return
}
for var minutelyForecastItem in forecastItems {
guard
let hourly = (location.hourly.first {
let thisHour = $0.date
let nextHour = thisHour.addingTimeInterval(3600)
return minutelyForecastItem.time >= thisHour && minutelyForecastItem.time < nextHour
})
else {
continue
}
minutelyForecastItem.weatherTypeImage = hourly.type.image(isDay: hourly.isDay)
minutelyForecast.append(minutelyForecastItem)
}
}
private func updateChart() {
verticalStackView.removeAll()
levelsStackView.removeAll()
levelsPositionXCache.removeAll()
levelsDashline.forEach { $0.removeFromSuperlayer() }
levelsDashline.removeAll()
scrollView.subviews.forEach {
if $0.isKind(of: UILabel.self) {
$0.removeFromSuperview()
}
}
switch forecastType {
case .temperature:
updateTemperatureChart()
case .precipitation:
updatePrecipitationChart()
}
levelsStackView.layoutIfNeeded()
verticalStackView.layoutIfNeeded()
//Levels dashline
for _ in 0..<verticalStackView.arrangedSubviews.count {
let levelShape = CAShapeLayer()
levelShape.opacity = 0.3
levelShape.lineWidth = 1
levelShape.lineDashPattern = [4, 5]
levelShape.strokeColor = UIColor(hex: 0x979797).cgColor
layer.insertSublayer(levelShape, at: 0)
levelsDashline.append(levelShape)
}
for (index, view) in levelsStackView.arrangedSubviews.enumerated() {
levelsPositionXCache[index] = view.frame.origin.x
}
}
private func updateTemperatureChart() {
guard
let maxTemp = (minutelyForecast.compactMap{ $0.temp }.max{ $0.value < $1.value} ),
let minTemp = (minutelyForecast.compactMap{ $0.temp }.min{ $0.value < $1.value} )
else {
return
}
var uniqTemps = minutelyForecast.compactMap{ $0.temp }.unique().sorted{$0.value > $1.value}
if uniqTemps.count > 4 {
let uniqMax = uniqTemps.removeFirst()
let uniqMin = uniqTemps.removeLast()
let midPoint = (uniqTemps.count / 2)
let firstHalf = uniqTemps[..<midPoint]
let firstHalfTemp = firstHalf[firstHalf.startIndex + firstHalf.count / 2]
let seconHalf = uniqTemps[midPoint...]
let secondHalfTemp = seconHalf[seconHalf.startIndex + seconHalf.count / 2]
uniqTemps = [uniqMax, firstHalfTemp, secondHalfTemp, uniqMin]
}
for temp in uniqTemps {
let label = UILabel()
label.text = temp.settingsConverted.shortString
label.font = AppFont.SFPro.regular(size: 10)
label.setContentHuggingPriority(.fittingSizeLevel, for: .vertical)
label.sizeToFit()
verticalStackView.addArrangedSubview(label)
}
for index in 0..<minutelyForecast.count {
let view = MinutelyLevelView(forecastType: .temperature)
levelsStackView.addArrangedSubview(view)
let diff = maxTemp.value - minTemp.value == 0 ? 1 : maxTemp.value - minTemp.value
let level = (0.05 + 0.9 * ((minutelyForecast[index].temp.value - minTemp.value) / diff))
view.snp.makeConstraints { make in
make.width.equalTo(kLevelWidth)
make.height.equalToSuperview().multipliedBy(level)
}
let minutes = Calendar.current.component(.minute, from: minutelyForecast[index].time)
if minutes % 20 == 0 {
let label = UILabel()
label.font = AppFont.SFPro.bold(size: 12)
label.text = dateFormatter.string(from: minutelyForecast[index].time)
scrollView.addSubview(label)
label.snp.makeConstraints { make in
make.top.equalTo(view.snp.bottom).offset(12)
make.centerX.equalTo(view)
}
}
}
}
private func updatePrecipitationChart() {
guard
let maxPrecip = (minutelyForecast.compactMap{ $0.precipitation }.max{ $0 < $1 }),
let minPrecip = (minutelyForecast.compactMap{ $0.precipitation }.min{ $0 < $1 })
else {
return
}
var uniqPrecips = minutelyForecast.compactMap{ $0.precipitation }.unique().sorted{$0 > $1}
if uniqPrecips.count > 4 {
let uniqMax = uniqPrecips.removeFirst()
let uniqMin = uniqPrecips.removeLast()
let midPoint = (uniqPrecips.count / 2)
let firstHalf = uniqPrecips[..<midPoint]
let firstHalfTemp = firstHalf[firstHalf.startIndex + firstHalf.count / 2]
let seconHalf = uniqPrecips[midPoint...]
let secondHalfTemp = seconHalf[seconHalf.startIndex + seconHalf.count / 2]
uniqPrecips = [uniqMax, firstHalfTemp, secondHalfTemp, uniqMin]
}
for precipitation in uniqPrecips {
let label = UILabel()
label.text = "\(Int(precipitation * 100))%"
label.font = AppFont.SFPro.regular(size: 10)
label.setContentHuggingPriority(.fittingSizeLevel, for: .vertical)
label.sizeToFit()
verticalStackView.addArrangedSubview(label)
}
for index in 0..<minutelyForecast.count {
let view = MinutelyLevelView(forecastType: .precipitation)
levelsStackView.addArrangedSubview(view)
let diff = maxPrecip - minPrecip == 0 ? 1 : maxPrecip - minPrecip
let level = (0.05 + 0.9 * ((minutelyForecast[index].precipitation - minPrecip) / diff))
view.snp.makeConstraints { make in
make.width.equalTo(kLevelWidth)
make.height.equalToSuperview().multipliedBy(level)
}
let minutes = Calendar.current.component(.minute, from: minutelyForecast[index].time)
if minutes % 20 == 0 {
let label = UILabel()
label.font = AppFont.SFPro.bold(size: 12)
label.text = dateFormatter.string(from: minutelyForecast[index].time)
scrollView.addSubview(label)
label.snp.makeConstraints { make in
make.top.equalTo(view.snp.bottom).offset(12)
make.centerX.equalTo(view)
}
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//MARK:- Prepare
private extension MinutelyForecastView {
func prepareDetailView() {
addSubview(detailsInfoView)
//Constraints
detailsInfoView.snp.makeConstraints { make in
make.width.equalTo(158)
make.height.equalTo(50)
make.top.equalToSuperview()
make.centerX.equalToSuperview()
}
}
func prepareVerticalStackView() {
verticalStackView.axis = .vertical
verticalStackView.distribution = .fillEqually
verticalStackView.alignment = .top
addSubview(verticalStackView)
verticalStackView.snp.makeConstraints { make in
make.height.equalTo(levelsStackView)
make.right.equalTo(scrollView.snp.left).offset(-6)
make.top.equalTo(scrollView)
}
}
func prepareScrollView() {
scrollView.backgroundColor = .clear
scrollView.delegate = self
addSubview(scrollView)
levelsStackView.axis = .horizontal
levelsStackView.spacing = 2
levelsStackView.distribution = .fill
levelsStackView.alignment = .bottom
scrollView.addSubview(levelsStackView)
levelsStackView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
make.height.equalToSuperview().multipliedBy(0.8)
}
scrollView.snp.makeConstraints { make in
make.left.equalToSuperview().inset(42)
make.top.equalTo(detailsInfoView.snp.bottom).offset(8)
make.right.equalToSuperview().inset(20)
make.bottom.equalToSuperview().inset(40)
}
}
func prepareCenterDashLine() {
centerDashline.lineWidth = 1
centerDashline.lineDashPattern = [4,2]
layer.addSublayer(centerDashline)
}
}
//MARK:- UIScrollView Delegate
extension MinutelyForecastView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let targetPointX = Double(scrollView.contentInset.left + scrollView.contentOffset.x)
guard let cachedValue = (levelsPositionXCache.first {
return $1...$1 + Double(kLevelWidth) ~= targetPointX
})
else {
return
}
self.updateDetailsView(minutelyItem: minutelyForecast[cachedValue.key])
if lastSelectedLevelIndex != cachedValue.key {
lastSelectedLevelIndex = cachedValue.key
feedbackGenerator.prepare()
feedbackGenerator.selectionChanged()
}
}
}
...@@ -21,6 +21,10 @@ private enum ForecastCellType { ...@@ -21,6 +21,10 @@ private enum ForecastCellType {
case precipitation case precipitation
case wind case wind
//Minutely
case minutely
case precipitationAdviced
// Shared // Shared
case adBanner case adBanner
case adMREC case adMREC
...@@ -156,7 +160,9 @@ class ForecastCellFactory: CellFactory { ...@@ -156,7 +160,9 @@ class ForecastCellFactory: CellFactory {
let cell = dequeueReusableCell(type: PrecipitationCell.self, tableView: tableView, indexPath: indexPath) let cell = dequeueReusableCell(type: PrecipitationCell.self, tableView: tableView, indexPath: indexPath)
if let hourly = forecastViewModel.location?.hourly { if let hourly = forecastViewModel.location?.hourly {
if cellsToUpdate.contains(.precipitation) { if cellsToUpdate.contains(.precipitation) {
cell.configure(with: hourly) if let location = forecastViewModel.location {
cell.configure(with: hourly, location: location)
}
cellsToUpdate.remove(.precipitation) cellsToUpdate.remove(.precipitation)
} }
} }
...@@ -170,6 +176,10 @@ class ForecastCellFactory: CellFactory { ...@@ -170,6 +176,10 @@ class ForecastCellFactory: CellFactory {
} }
} }
return cell return cell
case .minutely:
return UITableViewCell()
case .precipitationAdviced:
return UITableViewCell()
} }
} }
...@@ -179,6 +189,8 @@ class ForecastCellFactory: CellFactory { ...@@ -179,6 +189,8 @@ class ForecastCellFactory: CellFactory {
return self.dailySection.rows[indexPath.row] return self.dailySection.rows[indexPath.row]
case .hourly: case .hourly:
return self.hourlySection.rows[indexPath.row] return self.hourlySection.rows[indexPath.row]
default:
return .minutely
} }
} }
...@@ -256,6 +268,8 @@ class ForecastCellFactory: CellFactory { ...@@ -256,6 +268,8 @@ class ForecastCellFactory: CellFactory {
adTypeString = "MREC" adTypeString = "MREC"
adLoggingEmoji = "✅" adLoggingEmoji = "✅"
} }
default:
break
} }
adView.set(placementName: placementName, adType: adType) adView.set(placementName: placementName, adType: adType)
......
...@@ -70,6 +70,8 @@ class ForecastViewController: UIViewController { ...@@ -70,6 +70,8 @@ class ForecastViewController: UIViewController {
analytics(log: .ANALYTICS_VIEW_FORECAST_EXTENDED) analytics(log: .ANALYTICS_VIEW_FORECAST_EXTENDED)
case .hourly: case .hourly:
analytics(log: .ANALYTICS_VIEW_FORECAST_HOURLY) analytics(log: .ANALYTICS_VIEW_FORECAST_HOURLY)
default:
break
} }
} }
......
...@@ -161,7 +161,9 @@ class TodayCellFactory: CellFactory { ...@@ -161,7 +161,9 @@ class TodayCellFactory: CellFactory {
case .precipitation: case .precipitation:
let cell = dequeueReusableCell(type: PrecipitationCell.self, tableView: tableView, indexPath: indexPath) let cell = dequeueReusableCell(type: PrecipitationCell.self, tableView: tableView, indexPath: indexPath)
if cellsToUpdate.contains(.precipitation) { if cellsToUpdate.contains(.precipitation) {
cell.configure(with: loc.daily) if let location = todayViewModel.location {
cell.configure(with: loc.daily, location: location)
}
cellsToUpdate.remove(.precipitation) cellsToUpdate.remove(.precipitation)
} }
return cell return cell
......
...@@ -10,14 +10,18 @@ import OneWeatherCore ...@@ -10,14 +10,18 @@ import OneWeatherCore
class TodayForecastTimePeriodCell: UITableViewCell { class TodayForecastTimePeriodCell: UITableViewCell {
//Private //Private
private let periodSegmentedControl = ForecastTimePeriodControl(items: ["forecast.timePeriod.daily".localized(), // private let periodSegmentedControl = ForecastTimePeriodControl(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized()]) // "forecast.timePeriod.hourly".localized(),
// "forecast.timePeriod.minutely".localized()])
private let periodSegmentedControl = ForecastTimePeriodControl(items: nil)
private let forecastTimePeriodView = ForecastTimePeriodView() private let forecastTimePeriodView = ForecastTimePeriodView()
private let minutelyForecastView = MinutelyForecastView()
private let descriptionView = ForecastDescriptionView(lightStyleBackgroundColor: UIColor(hex: 0xfaedda).withAlphaComponent(0.5), private let descriptionView = ForecastDescriptionView(lightStyleBackgroundColor: UIColor(hex: 0xfaedda).withAlphaComponent(0.5),
gradientColors: [UIColor(hex: 0xe81e15).withAlphaComponent(0.33).cgColor, gradientColors: [UIColor(hex: 0xe81e15).withAlphaComponent(0.33).cgColor,
UIColor(hex: 0xf71d11).withAlphaComponent(0).cgColor]) UIColor(hex: 0xf71d11).withAlphaComponent(0).cgColor])
private var location:Location? private var location:Location?
private var graphIsDrawn = false private var graphIsDrawn = false
private let featureAvailability = FeatureAvailabilityManager.shared
//MARK:- Cell life cycle //MARK:- Cell life cycle
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
...@@ -26,6 +30,7 @@ class TodayForecastTimePeriodCell: UITableViewCell { ...@@ -26,6 +30,7 @@ class TodayForecastTimePeriodCell: UITableViewCell {
prepareCell() prepareCell()
prepareSegmentedControl() prepareSegmentedControl()
prepareForecastTimePeriodView() prepareForecastTimePeriodView()
prepareMinutelyForecastView()
prepareDescriptionView() prepareDescriptionView()
} }
...@@ -34,9 +39,22 @@ class TodayForecastTimePeriodCell: UITableViewCell { ...@@ -34,9 +39,22 @@ class TodayForecastTimePeriodCell: UITableViewCell {
} }
//Public //Public
public func configure(with location:Location) { public func configure(with location: Location) {
self.location = location self.location = location
//Update segment control
if featureAvailability?.isAvailable(feature: .minutelyForecast) == true {
self.periodSegmentedControl.set(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized(),
"forecast.timePeriod.minutely".localized()])
}
else {
self.periodSegmentedControl.set(items: ["forecast.timePeriod.daily".localized(),
"forecast.timePeriod.hourly".localized()])
}
self.forecastTimePeriodView.set(daily: location.daily, hourly: location.hourly) self.forecastTimePeriodView.set(daily: location.daily, hourly: location.hourly)
self.minutelyForecastView.configure(with: location, forecastType: .temperature)
self.handleSegmentDidChange() self.handleSegmentDidChange()
} }
...@@ -45,8 +63,16 @@ class TodayForecastTimePeriodCell: UITableViewCell { ...@@ -45,8 +63,16 @@ class TodayForecastTimePeriodCell: UITableViewCell {
return return
} }
let forecastType = timePeriod == .daily ? ForecastType.daily : ForecastType.hourly switch timePeriod {
self.forecastTimePeriodView.set(forecastType: forecastType, buttonType: ForecastPeriodButton.self) case .daily, .hourly:
forecastTimePeriodView.isHidden = false
minutelyForecastView.isHidden = true
let forecastType = timePeriod == .daily ? ForecastType.daily : ForecastType.hourly
self.forecastTimePeriodView.set(forecastType: forecastType, buttonType: ForecastPeriodButton.self)
case .minutely:
forecastTimePeriodView.isHidden = true
minutelyForecastView.isHidden = false
}
} }
} }
...@@ -78,6 +104,18 @@ private extension TodayForecastTimePeriodCell { ...@@ -78,6 +104,18 @@ private extension TodayForecastTimePeriodCell {
} }
} }
func prepareMinutelyForecastView() {
minutelyForecastView.isHidden = true
contentView.addSubview(minutelyForecastView)
minutelyForecastView.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.top.equalTo(periodSegmentedControl.snp.bottom).offset(40).priority(.medium)
make.height.equalTo(240)
}
}
func prepareDescriptionView() { func prepareDescriptionView() {
//TODO: Hide the description for now //TODO: Hide the description for now
descriptionView.isHidden = true descriptionView.isHidden = true
......
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
CDF079E026D4EBB300E797D9 /* BlendMinutelySource.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDF079D626D4EBB200E797D9 /* BlendMinutelySource.framework */; };
CDF079E526D4EBB300E797D9 /* BlendMinutelySourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF079E426D4EBB300E797D9 /* BlendMinutelySourceTests.swift */; };
CDF079E726D4EBB300E797D9 /* BlendMinutelySource.h in Headers */ = {isa = PBXBuildFile; fileRef = CDF079D926D4EBB200E797D9 /* BlendMinutelySource.h */; settings = {ATTRIBUTES = (Public, ); }; };
CDF079F826D4EE5000E797D9 /* OneWeatherCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDF079F726D4EE5000E797D9 /* OneWeatherCore.framework */; };
CDF079FC26D4EFED00E797D9 /* BlendMinutelySource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF079FB26D4EFED00E797D9 /* BlendMinutelySource.swift */; };
CDF07A0E26D50B4800E797D9 /* BlendMinutelyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF07A0C26D50B4700E797D9 /* BlendMinutelyItem.swift */; };
CDF07A0F26D50B4800E797D9 /* BlendMinutelyForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF07A0D26D50B4700E797D9 /* BlendMinutelyForecast.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
CDF079E126D4EBB300E797D9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = CDF079CD26D4EBB200E797D9 /* Project object */;
proxyType = 1;
remoteGlobalIDString = CDF079D526D4EBB200E797D9;
remoteInfo = BlendMinutelySource;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
CDF079D626D4EBB200E797D9 /* BlendMinutelySource.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BlendMinutelySource.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CDF079D926D4EBB200E797D9 /* BlendMinutelySource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BlendMinutelySource.h; sourceTree = "<group>"; };
CDF079DA26D4EBB200E797D9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CDF079DF26D4EBB300E797D9 /* BlendMinutelySourceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BlendMinutelySourceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
CDF079E426D4EBB300E797D9 /* BlendMinutelySourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlendMinutelySourceTests.swift; sourceTree = "<group>"; };
CDF079E626D4EBB300E797D9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CDF079F726D4EE5000E797D9 /* OneWeatherCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CDF079FB26D4EFED00E797D9 /* BlendMinutelySource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlendMinutelySource.swift; sourceTree = "<group>"; };
CDF07A0C26D50B4700E797D9 /* BlendMinutelyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlendMinutelyItem.swift; sourceTree = "<group>"; };
CDF07A0D26D50B4700E797D9 /* BlendMinutelyForecast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlendMinutelyForecast.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
CDF079D326D4EBB200E797D9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
CDF079F826D4EE5000E797D9 /* OneWeatherCore.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
CDF079DC26D4EBB300E797D9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
CDF079E026D4EBB300E797D9 /* BlendMinutelySource.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
CDF079CC26D4EBB200E797D9 = {
isa = PBXGroup;
children = (
CDF079D826D4EBB200E797D9 /* BlendMinutelySource */,
CDF079E326D4EBB300E797D9 /* BlendMinutelySourceTests */,
CDF079D726D4EBB200E797D9 /* Products */,
CDF079F626D4EE5000E797D9 /* Frameworks */,
);
sourceTree = "<group>";
};
CDF079D726D4EBB200E797D9 /* Products */ = {
isa = PBXGroup;
children = (
CDF079D626D4EBB200E797D9 /* BlendMinutelySource.framework */,
CDF079DF26D4EBB300E797D9 /* BlendMinutelySourceTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
CDF079D826D4EBB200E797D9 /* BlendMinutelySource */ = {
isa = PBXGroup;
children = (
CDF07A0726D5098300E797D9 /* Models */,
CDF079FB26D4EFED00E797D9 /* BlendMinutelySource.swift */,
CDF079D926D4EBB200E797D9 /* BlendMinutelySource.h */,
CDF079DA26D4EBB200E797D9 /* Info.plist */,
);
path = BlendMinutelySource;
sourceTree = "<group>";
};
CDF079E326D4EBB300E797D9 /* BlendMinutelySourceTests */ = {
isa = PBXGroup;
children = (
CDF079E426D4EBB300E797D9 /* BlendMinutelySourceTests.swift */,
CDF079E626D4EBB300E797D9 /* Info.plist */,
);
path = BlendMinutelySourceTests;
sourceTree = "<group>";
};
CDF079F626D4EE5000E797D9 /* Frameworks */ = {
isa = PBXGroup;
children = (
CDF079F726D4EE5000E797D9 /* OneWeatherCore.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
CDF07A0726D5098300E797D9 /* Models */ = {
isa = PBXGroup;
children = (
CDF07A0D26D50B4700E797D9 /* BlendMinutelyForecast.swift */,
CDF07A0C26D50B4700E797D9 /* BlendMinutelyItem.swift */,
);
path = Models;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
CDF079D126D4EBB200E797D9 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
CDF079E726D4EBB300E797D9 /* BlendMinutelySource.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
CDF079D526D4EBB200E797D9 /* BlendMinutelySource */ = {
isa = PBXNativeTarget;
buildConfigurationList = CDF079EA26D4EBB300E797D9 /* Build configuration list for PBXNativeTarget "BlendMinutelySource" */;
buildPhases = (
CDF079D126D4EBB200E797D9 /* Headers */,
CDF079D226D4EBB200E797D9 /* Sources */,
CDF079D326D4EBB200E797D9 /* Frameworks */,
CDF079D426D4EBB200E797D9 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = BlendMinutelySource;
productName = BlendMinutelySource;
productReference = CDF079D626D4EBB200E797D9 /* BlendMinutelySource.framework */;
productType = "com.apple.product-type.framework";
};
CDF079DE26D4EBB300E797D9 /* BlendMinutelySourceTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = CDF079ED26D4EBB300E797D9 /* Build configuration list for PBXNativeTarget "BlendMinutelySourceTests" */;
buildPhases = (
CDF079DB26D4EBB300E797D9 /* Sources */,
CDF079DC26D4EBB300E797D9 /* Frameworks */,
CDF079DD26D4EBB300E797D9 /* Resources */,
);
buildRules = (
);
dependencies = (
CDF079E226D4EBB300E797D9 /* PBXTargetDependency */,
);
name = BlendMinutelySourceTests;
productName = BlendMinutelySourceTests;
productReference = CDF079DF26D4EBB300E797D9 /* BlendMinutelySourceTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
CDF079CD26D4EBB200E797D9 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1250;
LastUpgradeCheck = 1250;
TargetAttributes = {
CDF079D526D4EBB200E797D9 = {
CreatedOnToolsVersion = 12.5.1;
LastSwiftMigration = 1250;
};
CDF079DE26D4EBB300E797D9 = {
CreatedOnToolsVersion = 12.5.1;
};
};
};
buildConfigurationList = CDF079D026D4EBB200E797D9 /* Build configuration list for PBXProject "BlendMinutelySource" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = CDF079CC26D4EBB200E797D9;
productRefGroup = CDF079D726D4EBB200E797D9 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
CDF079D526D4EBB200E797D9 /* BlendMinutelySource */,
CDF079DE26D4EBB300E797D9 /* BlendMinutelySourceTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
CDF079D426D4EBB200E797D9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
CDF079DD26D4EBB300E797D9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
CDF079D226D4EBB200E797D9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CDF079FC26D4EFED00E797D9 /* BlendMinutelySource.swift in Sources */,
CDF07A0F26D50B4800E797D9 /* BlendMinutelyForecast.swift in Sources */,
CDF07A0E26D50B4800E797D9 /* BlendMinutelyItem.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
CDF079DB26D4EBB300E797D9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CDF079E526D4EBB300E797D9 /* BlendMinutelySourceTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
CDF079E226D4EBB300E797D9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = CDF079D526D4EBB200E797D9 /* BlendMinutelySource */;
targetProxy = CDF079E126D4EBB300E797D9 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
CDF079E826D4EBB300E797D9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
CDF079E926D4EBB300E797D9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
CDF079EB26D4EBB300E797D9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 24W4XMQ38L;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = BlendMinutelySource/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 11.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.inmobi.BlendMinutelySource;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
CDF079EC26D4EBB300E797D9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 24W4XMQ38L;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = BlendMinutelySource/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 11.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.inmobi.BlendMinutelySource;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
CDF079EE26D4EBB300E797D9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 24W4XMQ38L;
INFOPLIST_FILE = BlendMinutelySourceTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.inmobi.BlendMinutelySourceTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
CDF079EF26D4EBB300E797D9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 24W4XMQ38L;
INFOPLIST_FILE = BlendMinutelySourceTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.inmobi.BlendMinutelySourceTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
CDF079D026D4EBB200E797D9 /* Build configuration list for PBXProject "BlendMinutelySource" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CDF079E826D4EBB300E797D9 /* Debug */,
CDF079E926D4EBB300E797D9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
CDF079EA26D4EBB300E797D9 /* Build configuration list for PBXNativeTarget "BlendMinutelySource" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CDF079EB26D4EBB300E797D9 /* Debug */,
CDF079EC26D4EBB300E797D9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
CDF079ED26D4EBB300E797D9 /* Build configuration list for PBXNativeTarget "BlendMinutelySourceTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CDF079EE26D4EBB300E797D9 /* Debug */,
CDF079EF26D4EBB300E797D9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = CDF079CD26D4EBB200E797D9 /* Project object */;
}
//
// BlendMinutelySource.h
// BlendMinutelySource
//
// Created by Dmitry Stepanets on 24.08.2021.
//
#import <Foundation/Foundation.h>
//! Project version number for BlendMinutelySource.
FOUNDATION_EXPORT double BlendMinutelySourceVersionNumber;
//! Project version string for BlendMinutelySource.
FOUNDATION_EXPORT const unsigned char BlendMinutelySourceVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <BlendMinutelySource/PublicHeader.h>
//
// BlendMinutelySource.swift
// BlendMinutelySource
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
import OneWeatherCore
public enum BlendMinutelySourceError: Error {
case badUrl
case networkError(Error?)
case badServerResponse(Error?)
case dataEncodingError(String)
case alreadyBeingUpdated
case invalidParameters
}
public class BlendMinutelySource: MinutelyForecastSource {
//Private
private let kBlendApiKey = "0imfnc8mVLWwsAawjYr4Rx-Af50DDqtlx"
private let kEndpoitURL = "https://pro-1w-dataaggregator.onelouder.com/1weather/api/v1/weather/nowcast"
private let kCountry = "US"
private let kWindUnit = "mph"
private let kPressureUnit = "inHg"
private let kPrecipitationUnit = "inhr"
private let kTempUnit = "F"
private lazy var dateFormatter: DateFormatter = {
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
return fmt
}()
/// This queue is needed to synchronize access to locationsBeingUpdated. Also, to make logging more clear.
private let internalQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = "BlendMinutelySource Queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
private var locationsBeingUpdated = Set<Location>()
//Public
#if DEBUG
public let minutelyUpdateInterval = TimeInterval(2 * 60) // 2 minutes
#else
public let minutelyUpdateInterval = TimeInterval(15 * 60) // 15 minutes
#endif
public init() {}
public func getMinutelyForecast(forLocation location: Location, completion: @escaping MinutelyForecastCompletion) {
internalQueue.addOperation { [weak self] in
let extendedCompletion: MinutelyForecastCompletion = { [weak self] result in
self?.internalQueue.addOperation {
completion(result)
self?.locationsBeingUpdated.remove(location)
}
}
self?.getMinutelyForecastInternal(forLocation: location, completion: extendedCompletion)
}
}
private func getMinutelyForecastInternal(forLocation location: Location, completion: @escaping MinutelyForecastCompletion) {
guard !locationsBeingUpdated.contains(location) else {
completion(.failure(BlendMinutelySourceError.alreadyBeingUpdated))
return
}
locationsBeingUpdated.insert(location)
guard location.region != nil, location.cityName != nil else {
completion(.failure(BlendMinutelySourceError.invalidParameters))
return
}
let endpointURL = URL(string: kEndpoitURL)!
let queryItems = [URLQueryItem(name: "lat", value: location.lat),
URLQueryItem(name: "lon", value: location.lon),
URLQueryItem(name: "state", value: location.region),
URLQueryItem(name: "country", value: kCountry),
URLQueryItem(name: "city", value: location.cityName),
URLQueryItem(name: "wind_unit", value: kWindUnit),
URLQueryItem(name: "pressure_unit", value: kPressureUnit),
URLQueryItem(name: "prec_unit", value: kPrecipitationUnit),
URLQueryItem(name: "temp_unit", value: kTempUnit)]
guard var components = URLComponents(url: endpointURL, resolvingAgainstBaseURL: true) else {
completion(.failure(BlendMinutelySourceError.badUrl))
return
}
components.queryItems = queryItems
guard let requestURL = components.url else {
completion(.failure(BlendMinutelySourceError.badUrl))
return
}
var request = URLRequest(url: requestURL)
request.addValue(kBlendApiKey, forHTTPHeaderField: "blend-api-key")
URLSession.shared.dataTask(with: request) {[weak self] data, response, error in
guard let self = self else {
completion(.failure(BlendMinutelySourceError.dataEncodingError("Missing self")))
return
}
if let networkError = error {
completion(.failure(BlendMinutelySourceError.networkError(networkError)))
return
}
guard let forecastData = data else {
completion(.failure(BlendMinutelySourceError.dataEncodingError("Incoming data is invalid")))
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(self.dateFormatter)
do {
let blendForecast = try decoder.decode(BlendMinutelyForecast.self, from: forecastData)
let forecast = self.covnertToAppModel(itemToConvert: blendForecast)
completion(.success(forecast))
}
catch {
guard let bodyData = data else {
completion(.failure(BlendMinutelySourceError.dataEncodingError(error.localizedDescription)))
return
}
let body = String(data: bodyData, encoding: .utf8) ?? "N/A"
completion(.failure(BlendMinutelySourceError.dataEncodingError(body)))
}
}
.resume()
}
private func covnertToAppModel(itemToConvert: BlendMinutelyForecast) -> MinutelyForecast {
let items = itemToConvert.forecast.map { MinutelyItem(time: $0.time,
temp: .init(value: Double($0.temp), unit: .fahrenheit),
precipitation: $0.precipitation,
windSpeed: .init(value: Double($0.windSpeed), unit: .milesPerHour),
pressure: .init(value: Double($0.pressure), unit: .inchesOfMercury)) }
let minutelyForecast = MinutelyForecast(lastUpdateTime: Date(),
forecast: items)
return minutelyForecast
}
}
<?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>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>
//
// BlendMinutelyForecast.swift
// BlendMinutelySource
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
import OneWeatherCore
struct BlendMinutelyForecast: Codable {
public let forecastInterval: Int
public let tempUnit: String
public let windUnit: String
public let pressureUnit: String
public let forecast: [BlendMinutelyItem]
}
//
// BlendMinutelyItem.swift
// BlendMinutelySource
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
public struct BlendMinutelyItem: Codable {
let time: Date
let temp: Int
let precipitation: Double
let windSpeed: Int
let pressure: Int
}
//
// BlendMinutelySourceTests.swift
// BlendMinutelySourceTests
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import XCTest
@testable import BlendMinutelySource
class BlendMinutelySourceTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
<?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>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
...@@ -39,6 +39,10 @@ ...@@ -39,6 +39,10 @@
CD8B861826F9C5EA00E3A9CD /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD8B861726F9C5EA00E3A9CD /* CoreLocation.framework */; }; CD8B861826F9C5EA00E3A9CD /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD8B861726F9C5EA00E3A9CD /* CoreLocation.framework */; };
CD8EA914265D42E2000D3D63 /* 1WModel.xcdatamodeld in Resources */ = {isa = PBXBuildFile; fileRef = CD3884372657BA420070FD6F /* 1WModel.xcdatamodeld */; }; CD8EA914265D42E2000D3D63 /* 1WModel.xcdatamodeld in Resources */ = {isa = PBXBuildFile; fileRef = CD3884372657BA420070FD6F /* 1WModel.xcdatamodeld */; };
CE0E006C26C739680060CBB6 /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0E006B26C739680060CBB6 /* CoreDataStack.swift */; }; CE0E006C26C739680060CBB6 /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0E006B26C739680060CBB6 /* CoreDataStack.swift */; };
CEC40AF42705A2BF00C98305 /* _CoreMinutelyForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC40AF22705A2BE00C98305 /* _CoreMinutelyForecast.swift */; };
CEC40AF52705A2BF00C98305 /* _CoreMinutelyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC40AF32705A2BE00C98305 /* _CoreMinutelyItem.swift */; };
CEC40AF82705A2C900C98305 /* CoreMinutelyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC40AF62705A2C900C98305 /* CoreMinutelyItem.swift */; };
CEC40AF92705A2C900C98305 /* CoreMinutelyForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC40AF72705A2C900C98305 /* CoreMinutelyForecast.swift */; };
CEEF40F9265E2EE600425D8F /* OneWeatherCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEF40F8265E2EE600425D8F /* OneWeatherCore.framework */; }; CEEF40F9265E2EE600425D8F /* OneWeatherCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEF40F8265E2EE600425D8F /* OneWeatherCore.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
...@@ -90,6 +94,11 @@ ...@@ -90,6 +94,11 @@
CD69DBC12666381500FD2A7C /* OneWeatherAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CD69DBC12666381500FD2A7C /* OneWeatherAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CD8B861726F9C5EA00E3A9CD /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; CD8B861726F9C5EA00E3A9CD /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; };
CE0E006B26C739680060CBB6 /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = "<group>"; }; CE0E006B26C739680060CBB6 /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = "<group>"; };
CEC40AF1270587F600C98305 /* 1.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = 1.2.xcdatamodel; sourceTree = "<group>"; };
CEC40AF22705A2BE00C98305 /* _CoreMinutelyForecast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _CoreMinutelyForecast.swift; sourceTree = "<group>"; };
CEC40AF32705A2BE00C98305 /* _CoreMinutelyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _CoreMinutelyItem.swift; sourceTree = "<group>"; };
CEC40AF62705A2C900C98305 /* CoreMinutelyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreMinutelyItem.swift; sourceTree = "<group>"; };
CEC40AF72705A2C900C98305 /* CoreMinutelyForecast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreMinutelyForecast.swift; sourceTree = "<group>"; };
CEEF40F8265E2EE600425D8F /* OneWeatherCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CEEF40F8265E2EE600425D8F /* OneWeatherCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E5F0E97C8CB8930C9E20B7FD /* Pods-CoreDataStorage.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreDataStorage.debug.xcconfig"; path = "Target Support Files/Pods-CoreDataStorage/Pods-CoreDataStorage.debug.xcconfig"; sourceTree = "<group>"; }; E5F0E97C8CB8930C9E20B7FD /* Pods-CoreDataStorage.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreDataStorage.debug.xcconfig"; path = "Target Support Files/Pods-CoreDataStorage/Pods-CoreDataStorage.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
...@@ -176,6 +185,8 @@ ...@@ -176,6 +185,8 @@
CD3884262657BA410070FD6F /* CoreHourlyWeather.swift */, CD3884262657BA410070FD6F /* CoreHourlyWeather.swift */,
CD3884272657BA410070FD6F /* CoreNotifications.swift */, CD3884272657BA410070FD6F /* CoreNotifications.swift */,
CD3884282657BA410070FD6F /* CoreNWSAlert.swift */, CD3884282657BA410070FD6F /* CoreNWSAlert.swift */,
CEC40AF72705A2C900C98305 /* CoreMinutelyForecast.swift */,
CEC40AF62705A2C900C98305 /* CoreMinutelyItem.swift */,
); );
path = Human; path = Human;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -193,6 +204,8 @@ ...@@ -193,6 +204,8 @@
CD3884312657BA410070FD6F /* _CoreDailyWeather.swift */, CD3884312657BA410070FD6F /* _CoreDailyWeather.swift */,
CD3884322657BA410070FD6F /* _CoreNotifications.swift */, CD3884322657BA410070FD6F /* _CoreNotifications.swift */,
CD3884332657BA410070FD6F /* _CoreNWSAlert.swift */, CD3884332657BA410070FD6F /* _CoreNWSAlert.swift */,
CEC40AF22705A2BE00C98305 /* _CoreMinutelyForecast.swift */,
CEC40AF32705A2BE00C98305 /* _CoreMinutelyItem.swift */,
); );
path = Machine; path = Machine;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -369,10 +382,12 @@ ...@@ -369,10 +382,12 @@
CD3884442657BA550070FD6F /* CoreHealth.swift in Sources */, CD3884442657BA550070FD6F /* CoreHealth.swift in Sources */,
CD3884452657BA550070FD6F /* CoreLocation.swift in Sources */, CD3884452657BA550070FD6F /* CoreLocation.swift in Sources */,
CD3884462657BA550070FD6F /* CoreDailyWeather.swift in Sources */, CD3884462657BA550070FD6F /* CoreDailyWeather.swift in Sources */,
CEC40AF52705A2BF00C98305 /* _CoreMinutelyItem.swift in Sources */,
CD3884472657BA550070FD6F /* CoreHourlyWeather.swift in Sources */, CD3884472657BA550070FD6F /* CoreHourlyWeather.swift in Sources */,
CD3884482657BA550070FD6F /* CoreNotifications.swift in Sources */, CD3884482657BA550070FD6F /* CoreNotifications.swift in Sources */,
CD3884492657BA550070FD6F /* CoreNWSAlert.swift in Sources */, CD3884492657BA550070FD6F /* CoreNWSAlert.swift in Sources */,
CD38844A2657BA550070FD6F /* _CoreAppData.swift in Sources */, CD38844A2657BA550070FD6F /* _CoreAppData.swift in Sources */,
CEC40AF82705A2C900C98305 /* CoreMinutelyItem.swift in Sources */,
CD38844B2657BA550070FD6F /* _CoreAirQuality.swift in Sources */, CD38844B2657BA550070FD6F /* _CoreAirQuality.swift in Sources */,
CD38844C2657BA550070FD6F /* _CorePollutant.swift in Sources */, CD38844C2657BA550070FD6F /* _CorePollutant.swift in Sources */,
CD38844D2657BA550070FD6F /* _CoreHourlyWeather.swift in Sources */, CD38844D2657BA550070FD6F /* _CoreHourlyWeather.swift in Sources */,
...@@ -380,11 +395,13 @@ ...@@ -380,11 +395,13 @@
CD38844F2657BA550070FD6F /* _CoreLocation.swift in Sources */, CD38844F2657BA550070FD6F /* _CoreLocation.swift in Sources */,
CD3884502657BA550070FD6F /* _CoreCurrentWeather.swift in Sources */, CD3884502657BA550070FD6F /* _CoreCurrentWeather.swift in Sources */,
CD3884512657BA550070FD6F /* _CoreDailyWeather.swift in Sources */, CD3884512657BA550070FD6F /* _CoreDailyWeather.swift in Sources */,
CEC40AF42705A2BF00C98305 /* _CoreMinutelyForecast.swift in Sources */,
CD3884522657BA550070FD6F /* _CoreNotifications.swift in Sources */, CD3884522657BA550070FD6F /* _CoreNotifications.swift in Sources */,
CD3884532657BA550070FD6F /* _CoreNWSAlert.swift in Sources */, CD3884532657BA550070FD6F /* _CoreNWSAlert.swift in Sources */,
CE0E006C26C739680060CBB6 /* CoreDataStack.swift in Sources */, CE0E006C26C739680060CBB6 /* CoreDataStack.swift in Sources */,
CD38843F2657BA430070FD6F /* CoreDataStorage.swift in Sources */, CD38843F2657BA430070FD6F /* CoreDataStorage.swift in Sources */,
CD38843B2657BA430070FD6F /* CoreDataAppModelConvertable.swift in Sources */, CD38843B2657BA430070FD6F /* CoreDataAppModelConvertable.swift in Sources */,
CEC40AF92705A2C900C98305 /* CoreMinutelyForecast.swift in Sources */,
CD38843D2657BA430070FD6F /* CoreDataError.swift in Sources */, CD38843D2657BA430070FD6F /* CoreDataError.swift in Sources */,
CD38843E2657BA430070FD6F /* CoreDataUtils.swift in Sources */, CD38843E2657BA430070FD6F /* CoreDataUtils.swift in Sources */,
); );
...@@ -659,9 +676,10 @@ ...@@ -659,9 +676,10 @@
CD3884372657BA420070FD6F /* 1WModel.xcdatamodeld */ = { CD3884372657BA420070FD6F /* 1WModel.xcdatamodeld */ = {
isa = XCVersionGroup; isa = XCVersionGroup;
children = ( children = (
CEC40AF1270587F600C98305 /* 1.2.xcdatamodel */,
CD3884362657BA420070FD6F /* Model.xcdatamodel */, CD3884362657BA420070FD6F /* Model.xcdatamodel */,
); );
currentVersion = CD3884362657BA420070FD6F /* Model.xcdatamodel */; currentVersion = CEC40AF1270587F600C98305 /* 1.2.xcdatamodel */;
path = 1WModel.xcdatamodeld; path = 1WModel.xcdatamodeld;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
......
<?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>
<key>_XCCurrentVersionName</key>
<string>1.2.xcdatamodel</string>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19206" systemVersion="20E241" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="CoreAirQuality" representedClassName="CoreAirQuality" syncable="YES">
<attribute name="advice" attributeType="String"/>
<attribute name="index" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="health" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CoreHealth" inverseName="airQuality" inverseEntity="CoreHealth"/>
</entity>
<entity name="CoreAppData" representedClassName="CoreAppData" syncable="YES">
<attribute name="selectedIndex" optional="YES" attributeType="Decimal" defaultValueString="0"/>
<relationship name="locations" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="CoreLocation" inverseName="appData" inverseEntity="CoreLocation"/>
</entity>
<entity name="CoreCurrentWeather" representedClassName="CoreCurrentWeather" syncable="YES">
<attribute name="apparentTemp" optional="YES" attributeType="Binary"/>
<attribute name="approximateMoonrise" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="dewPoint" optional="YES" attributeType="Binary"/>
<attribute name="humidity" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
<attribute name="isDay" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="lastTimeUpdated" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="maxTemp" optional="YES" attributeType="Binary"/>
<attribute name="minTemp" optional="YES" attributeType="Binary"/>
<attribute name="moonPhase" optional="YES" attributeType="String"/>
<attribute name="moonrise" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="moonset" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="moonState" optional="YES" attributeType="String"/>
<attribute name="precipitationProbability" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
<attribute name="pressure" optional="YES" attributeType="Binary"/>
<attribute name="sunrise" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sunset" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sunState" optional="YES" attributeType="String"/>
<attribute name="temp" optional="YES" attributeType="Binary"/>
<attribute name="timeZone" attributeType="String"/>
<attribute name="type" attributeType="String"/>
<attribute name="uv" optional="YES" attributeType="Decimal" defaultValueString="0"/>
<attribute name="visibility" optional="YES" attributeType="Binary"/>
<attribute name="weekDay" attributeType="String"/>
<attribute name="windDirection" optional="YES" attributeType="String"/>
<attribute name="windSpeed" optional="YES" attributeType="Binary"/>
<relationship name="location" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CoreLocation" inverseName="today" inverseEntity="CoreLocation"/>
</entity>
<entity name="CoreDailyWeather" representedClassName="CoreDailyWeather" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastTimeUpdated" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="maxTemp" optional="YES" attributeType="Binary"/>
<attribute name="minTemp" optional="YES" attributeType="Binary"/>
<attribute name="moonPhase" optional="YES" attributeType="String"/>
<attribute name="moonrise" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="moonset" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="moonState" optional="YES" attributeType="String"/>
<attribute name="precipitationProbability" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
<attribute name="sunrise" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sunset" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sunState" optional="YES" attributeType="String"/>
<attribute name="timeZone" attributeType="String"/>
<attribute name="type" attributeType="String"/>
<attribute name="weekDay" attributeType="String"/>
<attribute name="windDirection" optional="YES" attributeType="String"/>
<attribute name="windSpeed" optional="YES" attributeType="Binary"/>
<relationship name="location" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CoreLocation" inverseName="daily" inverseEntity="CoreLocation"/>
</entity>
<entity name="CoreHealth" representedClassName="CoreHealth" syncable="YES">
<attribute name="lastUpdateTime" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="airQuality" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CoreAirQuality" inverseName="health" inverseEntity="CoreAirQuality"/>
<relationship name="location" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CoreLocation" inverseName="health" inverseEntity="CoreLocation"/>
<relationship name="pollutants" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="CorePollutant" inverseName="health" inverseEntity="CorePollutant"/>
</entity>
<entity name="CoreHourlyWeather" representedClassName="CoreHourlyWeather" syncable="YES">
<attribute name="apparentTemp" optional="YES" attributeType="Binary"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="dewPoint" optional="YES" attributeType="Binary"/>
<attribute name="humidity" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
<attribute name="isDay" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="lastTimeUpdated" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="precipitationProbability" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
<attribute name="temp" optional="YES" attributeType="Binary"/>
<attribute name="timeZone" attributeType="String"/>
<attribute name="type" attributeType="String"/>
<attribute name="weekDay" attributeType="String"/>
<attribute name="windDirection" optional="YES" attributeType="String"/>
<attribute name="windSpeed" optional="YES" attributeType="Binary"/>
<relationship name="location" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CoreLocation" inverseName="hourly" inverseEntity="CoreLocation"/>
</entity>
<entity name="CoreLocation" representedClassName="CoreLocation" syncable="YES">
<attribute name="cityName" optional="YES" attributeType="String"/>
<attribute name="countryCode" optional="YES" attributeType="String"/>
<attribute name="countryName" optional="YES" attributeType="String"/>
<attribute name="deviceLocation" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fipsCode" optional="YES" attributeType="String"/>
<attribute name="imageName" optional="YES" attributeType="String"/>
<attribute name="lastWeatherUpdateDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="latitude" optional="YES" attributeType="Decimal"/>
<attribute name="longitude" optional="YES" attributeType="Decimal"/>
<attribute name="nickname" optional="YES" attributeType="String"/>
<attribute name="region" optional="YES" attributeType="String"/>
<attribute name="timeZone" attributeType="String"/>
<attribute name="zip" optional="YES" attributeType="String"/>
<relationship name="appData" maxCount="1" deletionRule="Nullify" destinationEntity="CoreAppData" inverseName="locations" inverseEntity="CoreAppData"/>
<relationship name="daily" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="CoreDailyWeather" inverseName="location" inverseEntity="CoreDailyWeather"/>
<relationship name="health" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CoreHealth" inverseName="location" inverseEntity="CoreHealth"/>
<relationship name="hourly" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="CoreHourlyWeather" inverseName="location" inverseEntity="CoreHourlyWeather"/>
<relationship name="minutely" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CoreMinutelyForecast" inverseName="location" inverseEntity="CoreMinutelyForecast"/>
<relationship name="notifications" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CoreNotifications" inverseName="location" inverseEntity="CoreNotifications"/>
<relationship name="today" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CoreCurrentWeather" inverseName="location" inverseEntity="CoreCurrentWeather"/>
</entity>
<entity name="CoreMinutelyForecast" representedClassName="CoreMinutelyForecast" syncable="YES">
<attribute name="lastUpdateTime" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="items" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="CoreMinutelyItem" inverseName="forecast" inverseEntity="CoreMinutelyItem"/>
<relationship name="location" maxCount="1" deletionRule="Nullify" destinationEntity="CoreLocation" inverseName="minutely" inverseEntity="CoreLocation"/>
</entity>
<entity name="CoreMinutelyItem" representedClassName="CoreMinutelyItem" syncable="YES">
<attribute name="precipitation" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="pressure" attributeType="Binary"/>
<attribute name="temp" attributeType="Binary"/>
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="windSpeed" attributeType="Binary"/>
<relationship name="forecast" maxCount="1" deletionRule="Nullify" destinationEntity="CoreMinutelyForecast" inverseName="items" inverseEntity="CoreMinutelyForecast"/>
</entity>
<entity name="CoreNotifications" representedClassName="CoreNotifications" syncable="YES">
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="location" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CoreLocation" inverseName="notifications" inverseEntity="CoreLocation"/>
<relationship name="nwsAlerts" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="CoreNWSAlert" inverseName="notifications" inverseEntity="CoreNWSAlert"/>
</entity>
<entity name="CoreNWSAlert" representedClassName="CoreNWSAlert" syncable="YES">
<attribute name="city" attributeType="String"/>
<attribute name="descriptionText" attributeType="String"/>
<attribute name="expires" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="messageID" attributeType="String"/>
<attribute name="messageURL" attributeType="String"/>
<attribute name="read" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="severityLevel" attributeType="String"/>
<attribute name="timeZone" attributeType="String"/>
<attribute name="weatherID" attributeType="String"/>
<attribute name="weatherMessage" optional="YES" attributeType="String"/>
<relationship name="notifications" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CoreNotifications" inverseName="nwsAlerts" inverseEntity="CoreNotifications"/>
</entity>
<entity name="CorePollutant" representedClassName="CorePollutant" syncable="YES">
<attribute name="name" attributeType="String"/>
<attribute name="value" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="health" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CoreHealth" inverseName="pollutants" inverseEntity="CoreHealth"/>
</entity>
<elements>
<element name="CoreAirQuality" positionX="438.48828125" positionY="-201.96875" width="128" height="88"/>
<element name="CoreAppData" positionX="114.79296875" positionY="-494.5078125" width="128" height="73"/>
<element name="CoreCurrentWeather" positionX="-105.953125" positionY="-203.82421875" width="128" height="448"/>
<element name="CoreDailyWeather" positionX="270.5546875" positionY="-505.83203125" width="128" height="313"/>
<element name="CoreHealth" positionX="277.1640625" positionY="-156.77734375" width="128" height="103"/>
<element name="CoreHourlyWeather" positionX="279.15234375" positionY="-29.234375" width="128" height="253"/>
<element name="CoreLocation" positionX="113.6640625" positionY="-337.08984375" width="128" height="329"/>
<element name="CoreNotifications" positionX="-153.28125" positionY="-336.6484375" width="128" height="88"/>
<element name="CoreNWSAlert" positionX="-315.77734375" positionY="-337.84375" width="128" height="208"/>
<element name="CorePollutant" positionX="438.171875" positionY="-103.41015625" width="128" height="88"/>
<element name="CoreMinutelyForecast" positionX="-90" positionY="-324" width="128" height="74"/>
<element name="CoreMinutelyItem" positionX="-81" positionY="-315" width="128" height="119"/>
</elements>
</model>
\ No newline at end of file
...@@ -37,6 +37,7 @@ open class CoreLocation: _CoreLocation, CoreDataAppModelConvertable { ...@@ -37,6 +37,7 @@ open class CoreLocation: _CoreLocation, CoreDataAppModelConvertable {
location.health = try self.health?.toAppModel() location.health = try self.health?.toAppModel()
location.notifications = try self.notifications?.toAppModel() location.notifications = try self.notifications?.toAppModel()
location.minutely = try self.minutely?.toAppModel()
return location return location
} }
...@@ -86,6 +87,9 @@ open class CoreLocation: _CoreLocation, CoreDataAppModelConvertable { ...@@ -86,6 +87,9 @@ open class CoreLocation: _CoreLocation, CoreDataAppModelConvertable {
self.notifications = skipIfError(attribute: "notifications", action: { self.notifications = skipIfError(attribute: "notifications", action: {
try CoreNotifications(context: context, appModel: appModel.notifications) try CoreNotifications(context: context, appModel: appModel.notifications)
}) })
self.minutely = skipIfError(attribute: "minutely", action: {
try CoreMinutelyForecast(context: context, appModel: appModel.minutely)
})
} }
private func skipIfError<T>(attribute: String, action: () throws -> T?) -> T? { private func skipIfError<T>(attribute: String, action: () throws -> T?) -> T? {
......
import Foundation
import CoreData
import OneWeatherCore
import OneWeatherAnalytics
@objc(CoreMinutelyForecast)
open class CoreMinutelyForecast: _CoreMinutelyForecast, CoreDataAppModelConvertable {
func toAppModel() throws -> MinutelyForecast {
let lastUpdateTime: Date = self.lastUpdateTime
var minutely = [MinutelyItem]()
try CoreDataUtils.foreach(in: items, of: self, attributeName: "minutely") { (coreMinutelyItem: CoreMinutelyItem) in
minutely.append(try coreMinutelyItem.toAppModel())
}
return MinutelyForecast(lastUpdateTime: lastUpdateTime, forecast: minutely)
}
/// This is here just so that we could inherit the generated init(managedObjectContext) convenience initializer.
public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
required public init?(context: NSManagedObjectContext, appModel: MinutelyForecast?) throws {
guard let appModel = appModel else {
return nil
}
self.init(managedObjectContext: context)
self.lastUpdateTime = appModel.lastUpdateTime
self.items = NSOrderedSet(array: appModel.forecast.compactMap { elem in
skipIfError(attribute: "minutely") {
try CoreMinutelyItem(context: context, appModel: elem)
}
})
}
private func skipIfError<T>(attribute: String, action: () throws -> T?) -> T? {
do {
let result = try action()
return result
}
catch {
Logger(componentName: "CoreLocation").error("Couldn't parse \(attribute) in CoreLocation due to error: \(error)")
return nil
}
}
typealias AppModel = MinutelyForecast
}
import Foundation
import CoreData
import OneWeatherCore
@objc(CoreMinutelyItem)
open class CoreMinutelyItem: _CoreMinutelyItem, CoreDataAppModelConvertable {
func toAppModel() throws -> MinutelyItem {
guard let temp: Temperature = try CoreDataUtils.measurement(from: self.temp, in: self, attributeName: "temp") else {
throw CoreDataError.LoadAttributeError(entity: self, attributeName: "temp", value: self.temp, nestedError: nil)
}
guard let windSpeed: WindSpeed = try CoreDataUtils.measurement(from: self.windSpeed, in: self, attributeName: "windSpeed") else {
throw CoreDataError.LoadAttributeError(entity: self, attributeName: "windSpeed", value: self.windSpeed, nestedError: nil)
}
guard let pressure: Pressure = try CoreDataUtils.measurement(from: self.pressure, in: self, attributeName: "pressure") else {
throw CoreDataError.LoadAttributeError(entity: self, attributeName: "pressure", value: self.pressure, nestedError: nil)
}
return MinutelyItem(time: time, temp: temp, precipitation: precipitation, windSpeed: windSpeed, pressure: pressure)
}
/// This is here just so that we could inherit the generated init(managedObjectContext) convenience initializer.
public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
required public init?(context: NSManagedObjectContext, appModel: MinutelyItem?) throws {
guard let appModel = appModel else {
return nil
}
self.init(managedObjectContext: context)
self.time = appModel.time
self.precipitation = appModel.precipitation
self.temp = try CoreDataUtils.measurementToData(appModel.temp, in: self, attributeName: "temp")
self.windSpeed = try CoreDataUtils.measurementToData(appModel.windSpeed, in: self, attributeName: "windSpeed")
self.pressure = try CoreDataUtils.measurementToData(appModel.pressure, in: self, attributeName: "pressure")
}
typealias AppModel = MinutelyItem
}
...@@ -25,6 +25,7 @@ public enum CoreLocationRelationships: String { ...@@ -25,6 +25,7 @@ public enum CoreLocationRelationships: String {
case daily = "daily" case daily = "daily"
case health = "health" case health = "health"
case hourly = "hourly" case hourly = "hourly"
case minutely = "minutely"
case notifications = "notifications" case notifications = "notifications"
case today = "today" case today = "today"
} }
...@@ -121,6 +122,9 @@ open class _CoreLocation: NSManagedObject { ...@@ -121,6 +122,9 @@ open class _CoreLocation: NSManagedObject {
} }
@NSManaged open @NSManaged open
var minutely: CoreMinutelyForecast?
@NSManaged open
var notifications: CoreNotifications? var notifications: CoreNotifications?
@NSManaged open @NSManaged open
......
// DO NOT EDIT. This file is machine-generated and constantly overwritten.
// Make changes to CoreMinutelyForecast.swift instead.
import Foundation
import CoreData
public enum CoreMinutelyForecastAttributes: String {
case lastUpdateTime = "lastUpdateTime"
}
public enum CoreMinutelyForecastRelationships: String {
case items = "items"
case location = "location"
}
open class _CoreMinutelyForecast: NSManagedObject {
// MARK: - Class methods
open class func entityName () -> String {
return "CoreMinutelyForecast"
}
open class func entity(managedObjectContext: NSManagedObjectContext) -> NSEntityDescription? {
return NSEntityDescription.entity(forEntityName: self.entityName(), in: managedObjectContext)
}
@nonobjc
open class func fetchRequest() -> NSFetchRequest<CoreMinutelyForecast> {
return NSFetchRequest(entityName: self.entityName())
}
// MARK: - Life cycle methods
public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public convenience init?(managedObjectContext: NSManagedObjectContext) {
guard let entity = _CoreMinutelyForecast.entity(managedObjectContext: managedObjectContext) else { return nil }
self.init(entity: entity, insertInto: managedObjectContext)
}
// MARK: - Properties
@NSManaged open
var lastUpdateTime: Date!
// MARK: - Relationships
@NSManaged open
var items: NSOrderedSet
open func itemsSet() -> NSMutableOrderedSet {
return self.items.mutableCopy() as! NSMutableOrderedSet
}
@NSManaged open
var location: CoreLocation
}
extension _CoreMinutelyForecast {
open func addItems(_ objects: NSOrderedSet) {
let mutable = self.items.mutableCopy() as! NSMutableOrderedSet
mutable.union(objects)
self.items = mutable.copy() as! NSOrderedSet
}
open func removeItems(_ objects: NSOrderedSet) {
let mutable = self.items.mutableCopy() as! NSMutableOrderedSet
mutable.minus(objects)
self.items = mutable.copy() as! NSOrderedSet
}
open func addItemsObject(_ value: CoreMinutelyItem) {
let mutable = self.items.mutableCopy() as! NSMutableOrderedSet
mutable.add(value)
self.items = mutable.copy() as! NSOrderedSet
}
open func removeItemsObject(_ value: CoreMinutelyItem) {
let mutable = self.items.mutableCopy() as! NSMutableOrderedSet
mutable.remove(value)
self.items = mutable.copy() as! NSOrderedSet
}
}
// DO NOT EDIT. This file is machine-generated and constantly overwritten.
// Make changes to CoreMinutelyItem.swift instead.
import Foundation
import CoreData
public enum CoreMinutelyItemAttributes: String {
case precipitation = "precipitation"
case pressure = "pressure"
case temp = "temp"
case time = "time"
case windSpeed = "windSpeed"
}
public enum CoreMinutelyItemRelationships: String {
case forecast = "forecast"
}
open class _CoreMinutelyItem: NSManagedObject {
// MARK: - Class methods
open class func entityName () -> String {
return "CoreMinutelyItem"
}
open class func entity(managedObjectContext: NSManagedObjectContext) -> NSEntityDescription? {
return NSEntityDescription.entity(forEntityName: self.entityName(), in: managedObjectContext)
}
@nonobjc
open class func fetchRequest() -> NSFetchRequest<CoreMinutelyItem> {
return NSFetchRequest(entityName: self.entityName())
}
// MARK: - Life cycle methods
public override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
public convenience init?(managedObjectContext: NSManagedObjectContext) {
guard let entity = _CoreMinutelyItem.entity(managedObjectContext: managedObjectContext) else { return nil }
self.init(entity: entity, insertInto: managedObjectContext)
}
// MARK: - Properties
@NSManaged open
var precipitation: Double
@NSManaged open
var pressure: Data!
@NSManaged open
var temp: Data!
@NSManaged open
var time: Date!
@NSManaged open
var windSpeed: Data!
// MARK: - Relationships
@NSManaged open
var forecast: CoreMinutelyForecast
}
...@@ -73,6 +73,9 @@ ...@@ -73,6 +73,9 @@
CDD2F8EF2665102B00B48322 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD2F8EE2665102B00B48322 /* LocationManager.swift */; }; CDD2F8EF2665102B00B48322 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD2F8EE2665102B00B48322 /* LocationManager.swift */; };
CDD2F8F12665112900B48322 /* DeviceLocationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD2F8F02665112800B48322 /* DeviceLocationMonitor.swift */; }; CDD2F8F12665112900B48322 /* DeviceLocationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD2F8F02665112800B48322 /* DeviceLocationMonitor.swift */; };
CDD2F8F62665117400B48322 /* NWSAlertsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD2F8F42665117400B48322 /* NWSAlertsManager.swift */; }; CDD2F8F62665117400B48322 /* NWSAlertsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD2F8F42665117400B48322 /* NWSAlertsManager.swift */; };
CDF07A0126D5027300E797D9 /* MinutelyForecastSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF07A0026D5027300E797D9 /* MinutelyForecastSource.swift */; };
CDF07A0526D5032800E797D9 /* MinutelyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF07A0326D5032800E797D9 /* MinutelyItem.swift */; };
CDF07A0626D5032800E797D9 /* MinutelyForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF07A0426D5032800E797D9 /* MinutelyForecast.swift */; };
CDFE458D26566BD50021A29F /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE458C26566BD50021A29F /* Storage.swift */; }; CDFE458D26566BD50021A29F /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE458C26566BD50021A29F /* Storage.swift */; };
CDFE459426566D7B0021A29F /* HealthSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE459326566D7B0021A29F /* HealthSource.swift */; }; CDFE459426566D7B0021A29F /* HealthSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE459326566D7B0021A29F /* HealthSource.swift */; };
CDFE459626566D860021A29F /* FIPSSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE459526566D860021A29F /* FIPSSource.swift */; }; CDFE459626566D860021A29F /* FIPSSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDFE459526566D860021A29F /* FIPSSource.swift */; };
...@@ -185,6 +188,9 @@ ...@@ -185,6 +188,9 @@
CDD2F8EE2665102B00B48322 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; }; CDD2F8EE2665102B00B48322 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
CDD2F8F02665112800B48322 /* DeviceLocationMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceLocationMonitor.swift; sourceTree = "<group>"; }; CDD2F8F02665112800B48322 /* DeviceLocationMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceLocationMonitor.swift; sourceTree = "<group>"; };
CDD2F8F42665117400B48322 /* NWSAlertsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NWSAlertsManager.swift; sourceTree = "<group>"; }; CDD2F8F42665117400B48322 /* NWSAlertsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NWSAlertsManager.swift; sourceTree = "<group>"; };
CDF07A0026D5027300E797D9 /* MinutelyForecastSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinutelyForecastSource.swift; sourceTree = "<group>"; };
CDF07A0326D5032800E797D9 /* MinutelyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinutelyItem.swift; sourceTree = "<group>"; };
CDF07A0426D5032800E797D9 /* MinutelyForecast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinutelyForecast.swift; sourceTree = "<group>"; };
CDFE458C26566BD50021A29F /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; }; CDFE458C26566BD50021A29F /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
CDFE459326566D7B0021A29F /* HealthSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthSource.swift; sourceTree = "<group>"; }; CDFE459326566D7B0021A29F /* HealthSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthSource.swift; sourceTree = "<group>"; };
CDFE459526566D860021A29F /* FIPSSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FIPSSource.swift; sourceTree = "<group>"; }; CDFE459526566D860021A29F /* FIPSSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FIPSSource.swift; sourceTree = "<group>"; };
...@@ -388,6 +394,7 @@ ...@@ -388,6 +394,7 @@
CD615F912655269200B717DB /* Health */, CD615F912655269200B717DB /* Health */,
CD615F932655269200B717DB /* Notifications */, CD615F932655269200B717DB /* Notifications */,
CD3883EA2657B82A0070FD6F /* FIPS */, CD3883EA2657B82A0070FD6F /* FIPS */,
CDF07A0226D502F500E797D9 /* Minutely */,
CD427D1A266F5F0500B4350A /* Shorts */, CD427D1A266F5F0500B4350A /* Shorts */,
); );
path = ModelObjects; path = ModelObjects;
...@@ -464,6 +471,15 @@ ...@@ -464,6 +471,15 @@
path = Managers; path = Managers;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CDF07A0226D502F500E797D9 /* Minutely */ = {
isa = PBXGroup;
children = (
CDF07A0426D5032800E797D9 /* MinutelyForecast.swift */,
CDF07A0326D5032800E797D9 /* MinutelyItem.swift */,
);
path = Minutely;
sourceTree = "<group>";
};
CDFE458B26566BC20021A29F /* Storage */ = { CDFE458B26566BC20021A29F /* Storage */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
...@@ -482,6 +498,7 @@ ...@@ -482,6 +498,7 @@
CDFE459526566D860021A29F /* FIPSSource.swift */, CDFE459526566D860021A29F /* FIPSSource.swift */,
CDFE459326566D7B0021A29F /* HealthSource.swift */, CDFE459326566D7B0021A29F /* HealthSource.swift */,
CD427D18266F5DCE00B4350A /* ShortsSource.swift */, CD427D18266F5DCE00B4350A /* ShortsSource.swift */,
CDF07A0026D5027300E797D9 /* MinutelyForecastSource.swift */,
); );
path = Sources; path = Sources;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -723,6 +740,7 @@ ...@@ -723,6 +740,7 @@
CDD2F8F12665112900B48322 /* DeviceLocationMonitor.swift in Sources */, CDD2F8F12665112900B48322 /* DeviceLocationMonitor.swift in Sources */,
CD2D55D626553384007B70F4 /* UserDefaultsValue.swift in Sources */, CD2D55D626553384007B70F4 /* UserDefaultsValue.swift in Sources */,
CD550FBA265531A100257FB5 /* RadarLayer.swift in Sources */, CD550FBA265531A100257FB5 /* RadarLayer.swift in Sources */,
CDF07A0626D5032800E797D9 /* MinutelyForecast.swift in Sources */,
CD550FBB265531A100257FB5 /* RadarLayerType.swift in Sources */, CD550FBB265531A100257FB5 /* RadarLayerType.swift in Sources */,
CE3D393A26E7A66200E7E738 /* SubscriptionConfig.swift in Sources */, CE3D393A26E7A66200E7E738 /* SubscriptionConfig.swift in Sources */,
CDBC243F2656740E00F9F4E2 /* AppData.swift in Sources */, CDBC243F2656740E00F9F4E2 /* AppData.swift in Sources */,
...@@ -745,6 +763,7 @@ ...@@ -745,6 +763,7 @@
CD91685726552FAE00EC04EF /* MulticastDelegate.swift in Sources */, CD91685726552FAE00EC04EF /* MulticastDelegate.swift in Sources */,
CDFE458D26566BD50021A29F /* Storage.swift in Sources */, CDFE458D26566BD50021A29F /* Storage.swift in Sources */,
CD615FBD2655295C00B717DB /* Measurement+String.swift in Sources */, CD615FBD2655295C00B717DB /* Measurement+String.swift in Sources */,
CDF07A0526D5032800E797D9 /* MinutelyItem.swift in Sources */,
CD615FBE2655295C00B717DB /* Calendar+TimeZone.swift in Sources */, CD615FBE2655295C00B717DB /* Calendar+TimeZone.swift in Sources */,
CD71B9C6265E629D00803DBB /* String+NewLine.swift in Sources */, CD71B9C6265E629D00803DBB /* String+NewLine.swift in Sources */,
CD91685F26552FEC00EC04EF /* Global.swift in Sources */, CD91685F26552FEC00EC04EF /* Global.swift in Sources */,
...@@ -759,6 +778,7 @@ ...@@ -759,6 +778,7 @@
CD427D19266F5DCE00B4350A /* ShortsSource.swift in Sources */, CD427D19266F5DCE00B4350A /* ShortsSource.swift in Sources */,
CD2D55D8265533F4007B70F4 /* UserDefaultsWrapper.swift in Sources */, CD2D55D8265533F4007B70F4 /* UserDefaultsWrapper.swift in Sources */,
CDD2F8F62665117400B48322 /* NWSAlertsManager.swift in Sources */, CDD2F8F62665117400B48322 /* NWSAlertsManager.swift in Sources */,
CDF07A0126D5027300E797D9 /* MinutelyForecastSource.swift in Sources */,
CE3A112726CD3CDE00D925C7 /* UserDefaults+OneWeather.swift in Sources */, CE3A112726CD3CDE00D925C7 /* UserDefaults+OneWeather.swift in Sources */,
CD11AFE726651BF900EC4BA0 /* LegacyWdtLocation.swift in Sources */, CD11AFE726651BF900EC4BA0 /* LegacyWdtLocation.swift in Sources */,
CD11AFE326651B6300EC4BA0 /* LegacyMigrationManager.swift in Sources */, CD11AFE326651B6300EC4BA0 /* LegacyMigrationManager.swift in Sources */,
......
...@@ -128,11 +128,15 @@ public class WidgetManager { ...@@ -128,11 +128,15 @@ public class WidgetManager {
} }
let name = WidgetOptions.name(for: option) let name = WidgetOptions.name(for: option)
var launchParams: [AnalyticsParameter : Any] = [.ANALYTICS_KEY_WIDGET_NAME : name]
if let location = LocationManager.shared.selectedLocation {
let smartText = smartTextProvider.smartText(for: location)
launchParams[.ANALYTICS_KEY_WIDGET_SMART_TEXT] = smartText
}
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_CARD_CLICK) AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_CARD_CLICK)
AppAnalytics.shared.log(event: .ANALYTICS_LAUNCH_FROM_WIDGET, AppAnalytics.shared.log(event: .ANALYTICS_LAUNCH_FROM_WIDGET, params: launchParams)
params: [.ANALYTICS_KEY_WIDGET_NAME : name])
AppAnalytics.shared.log(event: .ANALYTICS_WIDGET_LAUNCH_FROM,
params: [.ANALYTICS_KEY_WIDGET_NAME : name])
} }
public func logUpdate(forLocation location: Location?, kind: String, family: WidgetFamily) { public func logUpdate(forLocation location: Location?, kind: String, family: WidgetFamily) {
......
...@@ -23,6 +23,7 @@ public class LocationManager { ...@@ -23,6 +23,7 @@ public class LocationManager {
private let weatherUpdateSource: WeatherSource private let weatherUpdateSource: WeatherSource
private let healthSource: HealthSource private let healthSource: HealthSource
private let minutelyForecastSource: MinutelyForecastSource
private let fipsSource: FIPSSource private let fipsSource: FIPSSource
public let nwsAlertsManager: NWSAlertsManager public let nwsAlertsManager: NWSAlertsManager
private var pushNotificationsManager: PushNotificationsManagerProtocol? private var pushNotificationsManager: PushNotificationsManagerProtocol?
...@@ -30,6 +31,8 @@ public class LocationManager { ...@@ -30,6 +31,8 @@ public class LocationManager {
private let legacyMigrator = LegacyMigrationManager() private let legacyMigrator = LegacyMigrationManager()
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
region: "US",
cityName: "Cupertino",
timeZone: TimeZone(abbreviation: "PST")!) { timeZone: TimeZone(abbreviation: "PST")!) {
didSet { didSet {
if locations.count == 0 { if locations.count == 0 {
...@@ -209,9 +212,10 @@ public class LocationManager { ...@@ -209,9 +212,10 @@ public class LocationManager {
!locations.isEmpty || deviceLocationMonitor.hasLocationPermissions !locations.isEmpty || deviceLocationMonitor.hasLocationPermissions
} }
public init(weatherUpdateSource: WeatherSource, healthSource: HealthSource, nwsAlertsManager: NWSAlertsManager, fipsSource: FIPSSource, pushNotificationsManager: PushNotificationsManagerProtocol?, storage: Storage) { public init(weatherUpdateSource: WeatherSource, healthSource: HealthSource, minutelyForecastSource: MinutelyForecastSource, nwsAlertsManager: NWSAlertsManager, fipsSource: FIPSSource, pushNotificationsManager: PushNotificationsManagerProtocol?, storage: Storage) {
self.weatherUpdateSource = weatherUpdateSource self.weatherUpdateSource = weatherUpdateSource
self.healthSource = healthSource self.healthSource = healthSource
self.minutelyForecastSource = minutelyForecastSource
self.deviceLocationMonitor = DeviceLocationMonitor() self.deviceLocationMonitor = DeviceLocationMonitor()
self.nwsAlertsManager = nwsAlertsManager self.nwsAlertsManager = nwsAlertsManager
self.fipsSource = fipsSource self.fipsSource = fipsSource
...@@ -301,6 +305,7 @@ public class LocationManager { ...@@ -301,6 +305,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)
updateMinutelyForecast(for: defaultLocation)
return return
} }
log.info("Update all \(locations.count) locations if needed...") log.info("Update all \(locations.count) locations if needed...")
...@@ -312,6 +317,7 @@ public class LocationManager { ...@@ -312,6 +317,7 @@ public class LocationManager {
updateWeather(for: location, updateType: .preferIncremental) updateWeather(for: location, updateType: .preferIncremental)
} }
updateHealth(for: location) updateHealth(for: location)
updateMinutelyForecast(for: location)
updateNotifications(for: location) updateNotifications(for: location)
getFipsIfNeeded(for: location) getFipsIfNeeded(for: location)
} }
...@@ -369,6 +375,30 @@ public class LocationManager { ...@@ -369,6 +375,30 @@ public class LocationManager {
} }
} }
public func updateMinutelyForecast(for location: Location) {
if let lastTimeUpdated = location.minutely?.lastUpdateTime {
guard Date().timeIntervalSince(lastTimeUpdated) >= minutelyForecastSource.minutelyUpdateInterval else {
log.verbose("Update minutely forecast (\(location)): fresh enough (last updated at \(lastTimeUpdated)), skip update.")
return
}
}
log.info("Update minutely forecast for: \(location)")
minutelyForecastSource.getMinutelyForecast(forLocation: location) {[weak self] result in
guard let self = self else { return }
switch result {
case .success(let minutelyForecast):
self.makeChanges(to: location, in: "minutely") { (location) -> Location in
var updatedLocation = location
updatedLocation.minutely = minutelyForecast
return updatedLocation
}
case .failure(let error):
self.log.error("Update minutely (\(location) error: \(error)")
}
}
}
public func updateNotifications(for location: Location) { public func updateNotifications(for location: Location) {
if let lastTimeUpdated = location.notifications?.updatedAt { if let lastTimeUpdated = location.notifications?.updatedAt {
guard Date().timeIntervalSince(lastTimeUpdated) >= nwsAlertsManager.updateInterval else { guard Date().timeIntervalSince(lastTimeUpdated) >= nwsAlertsManager.updateInterval else {
......
...@@ -47,6 +47,7 @@ public struct Location { ...@@ -47,6 +47,7 @@ public struct Location {
public var today: CurrentWeather? public var today: CurrentWeather?
public var daily = [DailyWeather]() public var daily = [DailyWeather]()
public var minutely: MinutelyForecast?
public var hourly = [HourlyWeather]() { public var hourly = [HourlyWeather]() {
didSet { didSet {
let calendar = Calendar.timeZoneCalendar(timeZone: self.timeZone) let calendar = Calendar.timeZoneCalendar(timeZone: self.timeZone)
......
//
// MinutelyForecast.swift
// OneWeatherCore
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
public struct MinutelyForecast {
public let lastUpdateTime: Date
public let forecast: [MinutelyItem]
public init(lastUpdateTime: Date, forecast: [MinutelyItem]) {
self.lastUpdateTime = lastUpdateTime
self.forecast = forecast
}
}
//
// MinutelyItem.swift
// OneWeatherCore
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
import UIKit
public struct MinutelyItem {
public let time: Date
public let temp: Temperature
public let precipitation: Double
public let windSpeed: WindSpeed
public let pressure: Pressure
public var weatherTypeImage: UIImage?
public init(time: Date, temp: Temperature, precipitation: Double, windSpeed: WindSpeed, pressure: Pressure) {
self.time = time
self.temp = temp
self.precipitation = precipitation
self.windSpeed = windSpeed
self.pressure = pressure
}
}
...@@ -97,7 +97,7 @@ public class Settings { ...@@ -97,7 +97,7 @@ public class Settings {
@UserDefaultsBasicValue(key: "initial_onboarding_showed", userDefaults: UserDefaults.appDefaults) @UserDefaultsBasicValue(key: "initial_onboarding_showed", userDefaults: UserDefaults.appDefaults)
public var initialOnboardingShowed = false public var initialOnboardingShowed = false
@UserDefaultsBasicValue(key: "shorts_showed_swipeUp_count", userDefaults: UserDefaults.appDefaults) @UserDefaultsBasicValue(key: "shorts_showed_swipeUp_count", userDefaults: UserDefaults.appDefaults)
public var shortsSwipeUpNudgeShowedCount = 0 public var shortsSwipeUpNudgeShowedCount = 0
......
//
// MinutelyForecastSource.swift
// OneWeatherCore
//
// Created by Dmitry Stepanets on 24.08.2021.
//
import Foundation
public typealias MinutelyForecastCompletion = (_ result: Result<MinutelyForecast, Error>) -> ()
public protocol MinutelyForecastSource {
var minutelyUpdateInterval: TimeInterval { get }
func getMinutelyForecast(forLocation location: Location, completion: @escaping MinutelyForecastCompletion)
}
...@@ -31,7 +31,7 @@ public class WdtWeatherSource: WeatherSource { ...@@ -31,7 +31,7 @@ public class WdtWeatherSource: WeatherSource {
#else #else
public let weatherUpdateInterval = TimeInterval(15 * 60) // 15 minutes public let weatherUpdateInterval = TimeInterval(15 * 60) // 15 minutes
#endif #endif
public init() {} public init() {}
public func updateWeather(for location: Location, type: WeatherUpdateType, completion: @escaping WeatherSourceCompletion) { public func updateWeather(for location: Location, type: WeatherUpdateType, completion: @escaping WeatherSourceCompletion) {
......
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