diff --git a/Config.xcconfig b/Config.xcconfig index 7ec0c86..4eb586d 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -9,4 +9,4 @@ // Configuration settings file format documentation can be found at: // https://help.apple.com/xcode/#/dev745c5c974 -OPENWEATHERMAP_APP_ID = +WEATHER_API_KEY= diff --git a/DatWeatherDoe.xcodeproj/project.pbxproj b/DatWeatherDoe.xcodeproj/project.pbxproj index 8cfd71a..c71ba12 100644 --- a/DatWeatherDoe.xcodeproj/project.pbxproj +++ b/DatWeatherDoe.xcodeproj/project.pbxproj @@ -7,11 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 1DA0031C299BD1D100D749C9 /* CityWeatherURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA0031A299BD1D100D749C9 /* CityWeatherURLBuilder.swift */; }; - 1DA0031D299BD1D100D749C9 /* CityWeatherRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA0031B299BD1D100D749C9 /* CityWeatherRepository.swift */; }; - 1DA0031F299BD30E00D749C9 /* CityValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA0031E299BD30E00D749C9 /* CityValidator.swift */; }; - 1DBBAE7A299BDB72005429E5 /* CityWeatherURLBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBBAE79299BDB72005429E5 /* CityWeatherURLBuilderTests.swift */; }; - 1DBBAE7C299BDBFC005429E5 /* CityValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBBAE7B299BDBFC005429E5 /* CityValidatorTests.swift */; }; 2000D4062AD86D520052EDA6 /* WindSpeedConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2000D4052AD86D520052EDA6 /* WindSpeedConverter.swift */; }; 2000D4082AD86DAA0052EDA6 /* WindSpeedFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2000D4072AD86DAA0052EDA6 /* WindSpeedFormatter.swift */; }; 20012FA6267980EE00553B60 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = 20012FA5267980EE00553B60 /* Reachability */; }; @@ -29,15 +24,19 @@ 202B1024278D514E00ED6D42 /* MenuBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202B1023278D514E00ED6D42 /* MenuBuilder.swift */; }; 202B1029278D5A7100ED6D42 /* WeatherForecaster.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202B1028278D5A7100ED6D42 /* WeatherForecaster.swift */; }; 202B102B278D5DDC00ED6D42 /* PopoverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202B102A278D5DDC00ED6D42 /* PopoverManager.swift */; }; - 202B1030278D632800ED6D42 /* WeatherAppIDParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202B102F278D632800ED6D42 /* WeatherAppIDParser.swift */; }; + 202B1030278D632800ED6D42 /* APIKeyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202B102F278D632800ED6D42 /* APIKeyParser.swift */; }; 202C31A21C528FE7003D5E5A /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202C31A11C528FE7003D5E5A /* EventMonitor.swift */; }; + 2039B3DF2C278AA4006A6B6D /* SunriseSunsetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3DE2C278AA4006A6B6D /* SunriseSunsetData.swift */; }; + 2039B3E32C289697006A6B6D /* TemperatureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3E22C289697006A6B6D /* TemperatureData.swift */; }; + 2039B3E72C2896BD006A6B6D /* WindData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3E62C2896BD006A6B6D /* WindData.swift */; }; + 2039B3E92C2896CF006A6B6D /* ForecastData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3E82C2896CF006A6B6D /* ForecastData.swift */; }; + 2039B3EB2C2896D7006A6B6D /* ForecastTemperatureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3EA2C2896D7006A6B6D /* ForecastTemperatureData.swift */; }; + 2039B3EF2C28996D006A6B6D /* WeatherCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3EE2C28996D006A6B6D /* WeatherCondition.swift */; }; 2044E91E2867D3CF00AED55B /* TemperatureForecastTextBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2044E91D2867D3CF00AED55B /* TemperatureForecastTextBuilder.swift */; }; 204597682A84492400CF73CE /* TemperatureWithDegreesCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 204597672A84492400CF73CE /* TemperatureWithDegreesCreator.swift */; }; - 20459C621C5C4E2D004D0DC1 /* WeatherCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20459C611C5C4E2D004D0DC1 /* WeatherCondition.swift */; }; 20459C641C5C50DA004D0DC1 /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20459C631C5C50DA004D0DC1 /* ConfigManager.swift */; }; 206523C826597B120026C506 /* WeatherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206523C726597B120026C506 /* WeatherError.swift */; }; 206523D62659A92B0026C506 /* WeatherDataBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206523D52659A92B0026C506 /* WeatherDataBuilder.swift */; }; - 206523DB2659CD4E0026C506 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206523DA2659CD4E0026C506 /* Storage.swift */; }; 206523DD265ABDF30026C506 /* ConfigManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206523DC265ABDF30026C506 /* ConfigManagerTests.swift */; }; 206523FB265AD5730026C506 /* WeatherSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206523FA265AD5730026C506 /* WeatherSource.swift */; }; 206523FD265AF03E0026C506 /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206523FC265AF03E0026C506 /* RefreshInterval.swift */; }; @@ -46,25 +45,16 @@ 206FF1BB2BB4BB9400111EAE /* WeatherConditionPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206FF1BA2BB4BB9400111EAE /* WeatherConditionPosition.swift */; }; 2074949927A09278002AA589 /* WeatherURLBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2074949827A09278002AA589 /* WeatherURLBuilderTests.swift */; }; 2074949D27A09358002AA589 /* LocationWeatherURLBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2074949C27A09358002AA589 /* LocationWeatherURLBuilderTests.swift */; }; - 2074949F27A0944E002AA589 /* ZipCodeWeatherURLBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2074949E27A0944E002AA589 /* ZipCodeWeatherURLBuilderTests.swift */; }; 2077BC4D278DEBA300E0453C /* MenuBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2077BC4C278DEBA300E0453C /* MenuBarManager.swift */; }; 2077BC52278DF98800E0453C /* WeatherConditionTextMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2077BC51278DF98800E0453C /* WeatherConditionTextMapper.swift */; }; - 2078C2E42794BDF200946880 /* LocationWeatherURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2078C2E32794BDF200946880 /* LocationWeatherURLBuilder.swift */; }; - 2091740B2A9BD7E800BB63E0 /* ZipCodeValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2091740A2A9BD7E800BB63E0 /* ZipCodeValidator.swift */; }; - 2091740D2A9BD80300BB63E0 /* ZipCodeWeatherRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2091740C2A9BD80300BB63E0 /* ZipCodeWeatherRepository.swift */; }; - 2091740F2A9BD81700BB63E0 /* ZipCodeWeatherURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2091740E2A9BD81700BB63E0 /* ZipCodeWeatherURLBuilder.swift */; }; 209174102A9BDA4A00BB63E0 /* ConfigureViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2085263427E80B9E0017D7F4 /* ConfigureViewModel.swift */; }; - 209174112A9BDA5000BB63E0 /* TemperatureConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209F8A2F2790DE5B00EB5C45 /* TemperatureConverter.swift */; }; 209482C629934BFF00AF39D4 /* MeasurementUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209482C529934BFF00AF39D4 /* MeasurementUnit.swift */; }; 209F8A38279136D300EB5C45 /* LocationValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209F8A37279136D300EB5C45 /* LocationValidator.swift */; }; 209F8A3D27914A5900EB5C45 /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209F8A3C27914A5900EB5C45 /* NetworkClient.swift */; }; 209F8A4127915DBC00EB5C45 /* LocationCoordinatesWeatherRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209F8A4027915DBC00EB5C45 /* LocationCoordinatesWeatherRepository.swift */; }; - 20B3845C27A1CE3900F85482 /* ZipCodeValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B3845B27A1CE3900F85482 /* ZipCodeValidatorTests.swift */; }; 20B3845F27A1CFE800F85482 /* LocationValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B3845E27A1CFE800F85482 /* LocationValidatorTests.swift */; }; 20B3846127A1CFF300F85482 /* LocationParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B3846027A1CFF300F85482 /* LocationParserTests.swift */; }; 20B4680E27938B0600FC6050 /* StatusItemManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B4680D27938B0600FC6050 /* StatusItemManager.swift */; }; - 20B46810279392B200FC6050 /* WeatherSmokyConditionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B4680F279392B200FC6050 /* WeatherSmokyConditionBuilder.swift */; }; - 20B468122793940300FC6050 /* Date+Night.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B468112793940300FC6050 /* Date+Night.swift */; }; 20B46814279394FB00FC6050 /* WeatherTextBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B46813279394FB00FC6050 /* WeatherTextBuilder.swift */; }; 20B468192793989B00FC6050 /* HumidityTextBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B468182793989B00FC6050 /* HumidityTextBuilder.swift */; }; 20B4681B2793A7E300FC6050 /* TemperatureTextBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B4681A2793A7E300FC6050 /* TemperatureTextBuilder.swift */; }; @@ -74,7 +64,6 @@ 20B9CDCD27B8325900C42508 /* WeatherSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B9CDCC27B8325900C42508 /* WeatherSourceTests.swift */; }; 20B9CDCF27B8335A00C42508 /* TemperatureUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B9CDCE27B8335A00C42508 /* TemperatureUnitTests.swift */; }; 20B9CDD127B833EE00C42508 /* RefreshIntervalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B9CDD027B833EE00C42508 /* RefreshIntervalTests.swift */; }; - 20BBCDA4278B6E37007DEEB0 /* SmokyWeatherCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BBCDA3278B6E37007DEEB0 /* SmokyWeatherCondition.swift */; }; 20BBCDAA278B8A18007DEEB0 /* WeatherViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BBCDA9278B8A18007DEEB0 /* WeatherViewModel.swift */; }; 20BBCDAD278B8B28007DEEB0 /* WeatherViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BBCDAC278B8B28007DEEB0 /* WeatherViewModelType.swift */; }; 20BBCDAF278B92A7007DEEB0 /* SystemLocationFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BBCDAE278B92A7007DEEB0 /* SystemLocationFetcher.swift */; }; @@ -105,11 +94,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 1DA0031A299BD1D100D749C9 /* CityWeatherURLBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CityWeatherURLBuilder.swift; sourceTree = ""; }; - 1DA0031B299BD1D100D749C9 /* CityWeatherRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CityWeatherRepository.swift; sourceTree = ""; }; - 1DA0031E299BD30E00D749C9 /* CityValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityValidator.swift; sourceTree = ""; }; - 1DBBAE79299BDB72005429E5 /* CityWeatherURLBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityWeatherURLBuilderTests.swift; sourceTree = ""; }; - 1DBBAE7B299BDBFC005429E5 /* CityValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityValidatorTests.swift; sourceTree = ""; }; 2000D4052AD86D520052EDA6 /* WindSpeedConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindSpeedConverter.swift; sourceTree = ""; }; 2000D4072AD86DAA0052EDA6 /* WindSpeedFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindSpeedFormatter.swift; sourceTree = ""; }; 2005C054278CB4E40067BBD1 /* WeatherValidatorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherValidatorType.swift; sourceTree = ""; }; @@ -128,15 +112,19 @@ 202B1023278D514E00ED6D42 /* MenuBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBuilder.swift; sourceTree = ""; }; 202B1028278D5A7100ED6D42 /* WeatherForecaster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherForecaster.swift; sourceTree = ""; }; 202B102A278D5DDC00ED6D42 /* PopoverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverManager.swift; sourceTree = ""; }; - 202B102F278D632800ED6D42 /* WeatherAppIDParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherAppIDParser.swift; sourceTree = ""; }; + 202B102F278D632800ED6D42 /* APIKeyParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeyParser.swift; sourceTree = ""; }; 202C31A11C528FE7003D5E5A /* EventMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventMonitor.swift; sourceTree = ""; }; + 2039B3DE2C278AA4006A6B6D /* SunriseSunsetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunriseSunsetData.swift; sourceTree = ""; }; + 2039B3E22C289697006A6B6D /* TemperatureData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureData.swift; sourceTree = ""; }; + 2039B3E62C2896BD006A6B6D /* WindData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindData.swift; sourceTree = ""; }; + 2039B3E82C2896CF006A6B6D /* ForecastData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastData.swift; sourceTree = ""; }; + 2039B3EA2C2896D7006A6B6D /* ForecastTemperatureData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastTemperatureData.swift; sourceTree = ""; }; + 2039B3EE2C28996D006A6B6D /* WeatherCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherCondition.swift; sourceTree = ""; }; 2044E91D2867D3CF00AED55B /* TemperatureForecastTextBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureForecastTextBuilder.swift; sourceTree = ""; }; 204597672A84492400CF73CE /* TemperatureWithDegreesCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureWithDegreesCreator.swift; sourceTree = ""; }; - 20459C611C5C4E2D004D0DC1 /* WeatherCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeatherCondition.swift; sourceTree = ""; }; 20459C631C5C50DA004D0DC1 /* ConfigManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigManager.swift; sourceTree = ""; }; 206523C726597B120026C506 /* WeatherError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherError.swift; sourceTree = ""; }; 206523D52659A92B0026C506 /* WeatherDataBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherDataBuilder.swift; sourceTree = ""; }; - 206523DA2659CD4E0026C506 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; 206523DC265ABDF30026C506 /* ConfigManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigManagerTests.swift; sourceTree = ""; }; 206523FA265AD5730026C506 /* WeatherSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherSource.swift; sourceTree = ""; }; 206523FC265AF03E0026C506 /* RefreshInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshInterval.swift; sourceTree = ""; }; @@ -145,26 +133,17 @@ 206FF1BA2BB4BB9400111EAE /* WeatherConditionPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherConditionPosition.swift; sourceTree = ""; }; 2074949827A09278002AA589 /* WeatherURLBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherURLBuilderTests.swift; sourceTree = ""; }; 2074949C27A09358002AA589 /* LocationWeatherURLBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationWeatherURLBuilderTests.swift; sourceTree = ""; }; - 2074949E27A0944E002AA589 /* ZipCodeWeatherURLBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipCodeWeatherURLBuilderTests.swift; sourceTree = ""; }; 2077BC4C278DEBA300E0453C /* MenuBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarManager.swift; sourceTree = ""; }; 2077BC51278DF98800E0453C /* WeatherConditionTextMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherConditionTextMapper.swift; sourceTree = ""; }; - 2078C2E32794BDF200946880 /* LocationWeatherURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationWeatherURLBuilder.swift; sourceTree = ""; }; 207E989B26838D0D00DC2162 /* DatWeatherDoe.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatWeatherDoe.xctestplan; sourceTree = ""; }; 2085263427E80B9E0017D7F4 /* ConfigureViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigureViewModel.swift; sourceTree = ""; }; - 2091740A2A9BD7E800BB63E0 /* ZipCodeValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipCodeValidator.swift; sourceTree = ""; }; - 2091740C2A9BD80300BB63E0 /* ZipCodeWeatherRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipCodeWeatherRepository.swift; sourceTree = ""; }; - 2091740E2A9BD81700BB63E0 /* ZipCodeWeatherURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipCodeWeatherURLBuilder.swift; sourceTree = ""; }; 209482C529934BFF00AF39D4 /* MeasurementUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementUnit.swift; sourceTree = ""; }; - 209F8A2F2790DE5B00EB5C45 /* TemperatureConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureConverter.swift; sourceTree = ""; }; 209F8A37279136D300EB5C45 /* LocationValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationValidator.swift; sourceTree = ""; }; 209F8A3C27914A5900EB5C45 /* NetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClient.swift; sourceTree = ""; }; 209F8A4027915DBC00EB5C45 /* LocationCoordinatesWeatherRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCoordinatesWeatherRepository.swift; sourceTree = ""; }; - 20B3845B27A1CE3900F85482 /* ZipCodeValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipCodeValidatorTests.swift; sourceTree = ""; }; 20B3845E27A1CFE800F85482 /* LocationValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationValidatorTests.swift; sourceTree = ""; }; 20B3846027A1CFF300F85482 /* LocationParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationParserTests.swift; sourceTree = ""; }; 20B4680D27938B0600FC6050 /* StatusItemManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemManager.swift; sourceTree = ""; }; - 20B4680F279392B200FC6050 /* WeatherSmokyConditionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherSmokyConditionBuilder.swift; sourceTree = ""; }; - 20B468112793940300FC6050 /* Date+Night.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Night.swift"; sourceTree = ""; }; 20B46813279394FB00FC6050 /* WeatherTextBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTextBuilder.swift; sourceTree = ""; }; 20B468182793989B00FC6050 /* HumidityTextBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HumidityTextBuilder.swift; sourceTree = ""; }; 20B4681A2793A7E300FC6050 /* TemperatureTextBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureTextBuilder.swift; sourceTree = ""; }; @@ -177,7 +156,6 @@ 20B9CDCC27B8325900C42508 /* WeatherSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherSourceTests.swift; sourceTree = ""; }; 20B9CDCE27B8335A00C42508 /* TemperatureUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureUnitTests.swift; sourceTree = ""; }; 20B9CDD027B833EE00C42508 /* RefreshIntervalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshIntervalTests.swift; sourceTree = ""; }; - 20BBCDA3278B6E37007DEEB0 /* SmokyWeatherCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmokyWeatherCondition.swift; sourceTree = ""; }; 20BBCDA9278B8A18007DEEB0 /* WeatherViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModel.swift; sourceTree = ""; }; 20BBCDAC278B8B28007DEEB0 /* WeatherViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModelType.swift; sourceTree = ""; }; 20BBCDAE278B92A7007DEEB0 /* SystemLocationFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemLocationFetcher.swift; sourceTree = ""; }; @@ -231,25 +209,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 1DA00319299BD1D100D749C9 /* City */ = { - isa = PBXGroup; - children = ( - 1DA0031A299BD1D100D749C9 /* CityWeatherURLBuilder.swift */, - 1DA0031B299BD1D100D749C9 /* CityWeatherRepository.swift */, - 1DA0031E299BD30E00D749C9 /* CityValidator.swift */, - ); - path = City; - sourceTree = ""; - }; - 1DBBAE78299BDB49005429E5 /* City */ = { - isa = PBXGroup; - children = ( - 1DBBAE79299BDB72005429E5 /* CityWeatherURLBuilderTests.swift */, - 1DBBAE7B299BDBFC005429E5 /* CityValidatorTests.swift */, - ); - path = City; - sourceTree = ""; - }; 2000D4042AD86D450052EDA6 /* StatusItem */ = { isa = PBXGroup; children = ( @@ -263,9 +222,7 @@ 2005C050278CB4040067BBD1 /* Repository */ = { isa = PBXGroup; children = ( - 1DA00319299BD1D100D749C9 /* City */, 209F8A4227915EAA00EB5C45 /* Location */, - 209174092A9BD7CF00BB63E0 /* Zip Code */, 2005C058278CB5FC0067BBD1 /* WeatherURLBuilder.swift */, 20D8571A2A831D62005727BB /* WeatherRepositoryType.swift */, 2005C054278CB4E40067BBD1 /* WeatherValidatorType.swift */, @@ -313,9 +270,7 @@ children = ( 2078C2E72794C5EC00946880 /* Builder */, 20AF0D952795251700AA7D18 /* Mapping */, - 20459C611C5C4E2D004D0DC1 /* WeatherCondition.swift */, - 20BBCDA3278B6E37007DEEB0 /* SmokyWeatherCondition.swift */, - 20B468112793940300FC6050 /* Date+Night.swift */, + 2039B3EE2C28996D006A6B6D /* WeatherCondition.swift */, ); path = Condition; sourceTree = ""; @@ -381,8 +336,7 @@ children = ( 206E15242A7C4C5C0096D33C /* ConfigOptions.swift */, 20459C631C5C50DA004D0DC1 /* ConfigManager.swift */, - 206523DA2659CD4E0026C506 /* Storage.swift */, - 202B102F278D632800ED6D42 /* WeatherAppIDParser.swift */, + 202B102F278D632800ED6D42 /* APIKeyParser.swift */, ); path = Config; sourceTree = ""; @@ -417,9 +371,7 @@ 2074949727A0926E002AA589 /* Repository */ = { isa = PBXGroup; children = ( - 1DBBAE78299BDB49005429E5 /* City */, 2074949A27A09337002AA589 /* Location */, - 2074949B27A0933B002AA589 /* Zip Code */, 2074949827A09278002AA589 /* WeatherURLBuilderTests.swift */, ); path = Repository; @@ -434,34 +386,14 @@ path = Location; sourceTree = ""; }; - 2074949B27A0933B002AA589 /* Zip Code */ = { - isa = PBXGroup; - children = ( - 2074949E27A0944E002AA589 /* ZipCodeWeatherURLBuilderTests.swift */, - 20B3845B27A1CE3900F85482 /* ZipCodeValidatorTests.swift */, - ); - path = "Zip Code"; - sourceTree = ""; - }; 2078C2E72794C5EC00946880 /* Builder */ = { isa = PBXGroup; children = ( 202B1014278D46AB00ED6D42 /* WeatherConditionBuilder.swift */, - 20B4680F279392B200FC6050 /* WeatherSmokyConditionBuilder.swift */, ); path = Builder; sourceTree = ""; }; - 209174092A9BD7CF00BB63E0 /* Zip Code */ = { - isa = PBXGroup; - children = ( - 2091740A2A9BD7E800BB63E0 /* ZipCodeValidator.swift */, - 2091740C2A9BD80300BB63E0 /* ZipCodeWeatherRepository.swift */, - 2091740E2A9BD81700BB63E0 /* ZipCodeWeatherURLBuilder.swift */, - ); - path = "Zip Code"; - sourceTree = ""; - }; 209F8A322790EC3400EB5C45 /* Popover */ = { isa = PBXGroup; children = ( @@ -484,6 +416,11 @@ children = ( 201C6C4520262E380065E795 /* WeatherAPIResponse.swift */, 2005C05C278CE0350067BBD1 /* WeatherAPIResponseParser.swift */, + 2039B3DE2C278AA4006A6B6D /* SunriseSunsetData.swift */, + 2039B3E22C289697006A6B6D /* TemperatureData.swift */, + 2039B3E62C2896BD006A6B6D /* WindData.swift */, + 2039B3E82C2896CF006A6B6D /* ForecastData.swift */, + 2039B3EA2C2896D7006A6B6D /* ForecastTemperatureData.swift */, ); path = Response; sourceTree = ""; @@ -493,7 +430,6 @@ children = ( 20B468202793B81700FC6050 /* Coordinates */, 20B4681F2793B81200FC6050 /* System */, - 2078C2E32794BDF200946880 /* LocationWeatherURLBuilder.swift */, ); path = Location; sourceTree = ""; @@ -543,7 +479,6 @@ isa = PBXGroup; children = ( 20B4681A2793A7E300FC6050 /* TemperatureTextBuilder.swift */, - 209F8A2F2790DE5B00EB5C45 /* TemperatureConverter.swift */, 20CA6E10278F49AC00FFC53A /* TemperatureFormatter.swift */, 2044E91D2867D3CF00AED55B /* TemperatureForecastTextBuilder.swift */, 204597672A84492400CF73CE /* TemperatureWithDegreesCreator.swift */, @@ -781,9 +716,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 209174112A9BDA5000BB63E0 /* TemperatureConverter.swift in Sources */, 209174102A9BDA4A00BB63E0 /* ConfigureViewModel.swift in Sources */, - 20459C621C5C4E2D004D0DC1 /* WeatherCondition.swift in Sources */, 2005C055278CB4E40067BBD1 /* WeatherValidatorType.swift in Sources */, 20B468222793B85900FC6050 /* SystemLocationWeatherRepository.swift in Sources */, 20BBCDAF278B92A7007DEEB0 /* SystemLocationFetcher.swift in Sources */, @@ -791,36 +724,31 @@ 2005C057278CB5640067BBD1 /* LocationParser.swift in Sources */, 2005C05D278CE0350067BBD1 /* WeatherAPIResponseParser.swift in Sources */, 2000D4082AD86DAA0052EDA6 /* WindSpeedFormatter.swift in Sources */, - 206523DB2659CD4E0026C506 /* Storage.swift in Sources */, 20459C641C5C50DA004D0DC1 /* ConfigManager.swift in Sources */, + 2039B3E32C289697006A6B6D /* TemperatureData.swift in Sources */, 2077BC4D278DEBA300E0453C /* MenuBarManager.swift in Sources */, 20B4681B2793A7E300FC6050 /* TemperatureTextBuilder.swift in Sources */, 20BBCDAA278B8A18007DEEB0 /* WeatherViewModel.swift in Sources */, - 20B468122793940300FC6050 /* Date+Night.swift in Sources */, - 2078C2E42794BDF200946880 /* LocationWeatherURLBuilder.swift in Sources */, - 1DA0031D299BD1D100D749C9 /* CityWeatherRepository.swift in Sources */, + 2039B3EF2C28996D006A6B6D /* WeatherCondition.swift in Sources */, 204597682A84492400CF73CE /* TemperatureWithDegreesCreator.swift in Sources */, E3BE2C91292C505A00C4F468 /* DropdownIcon.swift in Sources */, 202C31A21C528FE7003D5E5A /* EventMonitor.swift in Sources */, - 202B1030278D632800ED6D42 /* WeatherAppIDParser.swift in Sources */, + 202B1030278D632800ED6D42 /* APIKeyParser.swift in Sources */, 209F8A4127915DBC00EB5C45 /* LocationCoordinatesWeatherRepository.swift in Sources */, E3105BA62818805A00FB4C55 /* SunriseAndSunsetTextBuilder.swift in Sources */, 202B1024278D514E00ED6D42 /* MenuBuilder.swift in Sources */, 202B101E278D4F1900ED6D42 /* WeatherReachability.swift in Sources */, - 1DA0031C299BD1D100D749C9 /* CityWeatherURLBuilder.swift in Sources */, + 2039B3DF2C278AA4006A6B6D /* SunriseSunsetData.swift in Sources */, 20B46814279394FB00FC6050 /* WeatherTextBuilder.swift in Sources */, 20CA6E11278F49AC00FFC53A /* TemperatureFormatter.swift in Sources */, 2023EDA71C4ED09C0087FD67 /* AppDelegate.swift in Sources */, 2077BC52278DF98800E0453C /* WeatherConditionTextMapper.swift in Sources */, 20BBCDAD278B8B28007DEEB0 /* WeatherViewModelType.swift in Sources */, - 2091740F2A9BD81700BB63E0 /* ZipCodeWeatherURLBuilder.swift in Sources */, 2000D4062AD86D520052EDA6 /* WindSpeedConverter.swift in Sources */, 20D8571F2A832AC6005727BB /* TemperatureUnit.swift in Sources */, 20D857192A831B37005727BB /* WindDirectionMapper.swift in Sources */, 20D857172A831B1F005727BB /* WindDirection.swift in Sources */, 20D857102A8317F5005727BB /* ConfigureUnitOptionsView.swift in Sources */, - 2091740D2A9BD80300BB63E0 /* ZipCodeWeatherRepository.swift in Sources */, - 2091740B2A9BD7E800BB63E0 /* ZipCodeValidator.swift in Sources */, 202B1029278D5A7100ED6D42 /* WeatherForecaster.swift in Sources */, 206523C826597B120026C506 /* WeatherError.swift in Sources */, 209F8A38279136D300EB5C45 /* LocationValidator.swift in Sources */, @@ -828,23 +756,23 @@ 2044E91E2867D3CF00AED55B /* TemperatureForecastTextBuilder.swift in Sources */, 20D857142A831A16005727BB /* ConfigureValueSeparatorOptionsView.swift in Sources */, 201C6C4620262E380065E795 /* WeatherAPIResponse.swift in Sources */, - 1DA0031F299BD30E00D749C9 /* CityValidator.swift in Sources */, 206E15252A7C4C5C0096D33C /* ConfigOptions.swift in Sources */, 202B102B278D5DDC00ED6D42 /* PopoverManager.swift in Sources */, + 2039B3E72C2896BD006A6B6D /* WindData.swift in Sources */, 2005C059278CB5FC0067BBD1 /* WeatherURLBuilder.swift in Sources */, E3BE2C93292C50ED00C4F468 /* DropdownIconMapper.swift in Sources */, 20D857122A831802005727BB /* ConfigureWeatherOptionsView.swift in Sources */, 20D8571D2A831F40005727BB /* WeatherRepositoryFactory.swift in Sources */, - 20B46810279392B200FC6050 /* WeatherSmokyConditionBuilder.swift in Sources */, 20B468192793989B00FC6050 /* HumidityTextBuilder.swift in Sources */, 206523D62659A92B0026C506 /* WeatherDataBuilder.swift in Sources */, 206FF1BB2BB4BB9400111EAE /* WeatherConditionPosition.swift in Sources */, 20206F0727BFF3D7004B418F /* ConfigureView.swift in Sources */, - 20BBCDA4278B6E37007DEEB0 /* SmokyWeatherCondition.swift in Sources */, + 2039B3EB2C2896D7006A6B6D /* ForecastTemperatureData.swift in Sources */, 20F17D3A26597A02003A164E /* WeatherData.swift in Sources */, 202B1015278D46AB00ED6D42 /* WeatherConditionBuilder.swift in Sources */, 20E4E18529AC719E00860C18 /* WeatherConditionImageMapper.swift in Sources */, 206E152F2A7C544D0096D33C /* ConfigureOptionsView.swift in Sources */, + 2039B3E92C2896CF006A6B6D /* ForecastData.swift in Sources */, 20B4680E27938B0600FC6050 /* StatusItemManager.swift in Sources */, 209F8A3D27914A5900EB5C45 /* NetworkClient.swift in Sources */, 206523FD265AF03E0026C506 /* RefreshInterval.swift in Sources */, @@ -858,16 +786,12 @@ files = ( 20B3846127A1CFF300F85482 /* LocationParserTests.swift in Sources */, 2074949D27A09358002AA589 /* LocationWeatherURLBuilderTests.swift in Sources */, - 2074949F27A0944E002AA589 /* ZipCodeWeatherURLBuilderTests.swift in Sources */, 2074949927A09278002AA589 /* WeatherURLBuilderTests.swift in Sources */, 20B9CDCF27B8335A00C42508 /* TemperatureUnitTests.swift in Sources */, - 1DBBAE7A299BDB72005429E5 /* CityWeatherURLBuilderTests.swift in Sources */, - 20B3845C27A1CE3900F85482 /* ZipCodeValidatorTests.swift in Sources */, 206523DD265ABDF30026C506 /* ConfigManagerTests.swift in Sources */, 20B3845F27A1CFE800F85482 /* LocationValidatorTests.swift in Sources */, 20B9CDD127B833EE00C42508 /* RefreshIntervalTests.swift in Sources */, 20B9CDCD27B8325900C42508 /* WeatherSourceTests.swift in Sources */, - 1DBBAE7C299BDBFC005429E5 /* CityValidatorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1056,7 +980,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 4.2.3; + MARKETING_VERSION = 5.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.inderdhir.DatWeatherDoe.debug; PRODUCT_NAME = DatWeatherDoe; PROVISIONING_PROFILE = ""; @@ -1084,7 +1008,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 4.2.3; + MARKETING_VERSION = 5.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.inderdhir.DatWeatherDoe; PRODUCT_NAME = DatWeatherDoe; PROVISIONING_PROFILE = ""; @@ -1166,7 +1090,7 @@ repositoryURL = "https://github.com/nicklockwood/SwiftFormat"; requirement = { kind = exactVersion; - version = 0.53.8; + version = 0.54.0; }; }; 20012FA4267980EE00553B60 /* XCRemoteSwiftPackageReference "Reachability" */ = { @@ -1174,7 +1098,7 @@ repositoryURL = "https://github.com/ashleymills/Reachability.swift"; requirement = { kind = exactVersion; - version = 5.1.0; + version = 5.2.3; }; }; 20CB68BC2A2D9029001C73B9 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { @@ -1182,7 +1106,7 @@ repositoryURL = "https://github.com/realm/SwiftLint"; requirement = { kind = exactVersion; - version = 0.54.0; + version = 0.55.1; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/DatWeatherDoe.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DatWeatherDoe.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 89bd87f..550966f 100644 --- a/DatWeatherDoe.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DatWeatherDoe.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ashleymills/Reachability.swift", "state" : { - "revision" : "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2", - "version" : "5.1.0" + "revision" : "7cbd73f46a7dfaeca079e18df7324c6de6d1834a", + "version" : "5.2.3" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/SourceKitten.git", "state" : { - "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", - "version" : "0.34.1" + "revision" : "fd4df99170f5e9d7cf9aa8312aa8506e0e7a44e7", + "version" : "0.35.0" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version" : "509.0.2" + "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", + "version" : "510.0.2" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nicklockwood/SwiftFormat", "state" : { - "revision" : "ab238886b8b50f8b678b251f3c28c0c887305407", - "version" : "0.53.8" + "revision" : "dd989a46d0c6f15c016484bab8afe5e7a67a4022", + "version" : "0.54.0" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/SwiftLint", "state" : { - "revision" : "f17a4f9dfb6a6afb0408426354e4180daaf49cee", - "version" : "0.54.0" + "revision" : "b515723b16eba33f15c4677ee65f3fef2ce8c255", + "version" : "0.55.1" } }, { diff --git a/DatWeatherDoe/API/Response/ForecastData.swift b/DatWeatherDoe/API/Response/ForecastData.swift new file mode 100644 index 0000000..800978b --- /dev/null +++ b/DatWeatherDoe/API/Response/ForecastData.swift @@ -0,0 +1,27 @@ +// +// ForecastData.swift +// DatWeatherDoe +// +// Created by Inder Dhir on 6/23/24. +// Copyright © 2024 Inder Dhir. All rights reserved. +// + +import Foundation + +struct Forecast: Decodable { + let dayDataArr: [ForecastDayData] + + private enum CodingKeys: String, CodingKey { + case dayDataArr = "forecastday" + } +} + +struct ForecastDayData: Decodable { + let temp: ForecastTemperatureData + let astro: SunriseSunsetData + + private enum CodingKeys: String, CodingKey { + case temp = "day" + case astro + } +} diff --git a/DatWeatherDoe/API/Response/ForecastTemperatureData.swift b/DatWeatherDoe/API/Response/ForecastTemperatureData.swift new file mode 100644 index 0000000..5422828 --- /dev/null +++ b/DatWeatherDoe/API/Response/ForecastTemperatureData.swift @@ -0,0 +1,23 @@ +// +// ForecastTemperatureData.swift +// DatWeatherDoe +// +// Created by Inder Dhir on 6/23/24. +// Copyright © 2024 Inder Dhir. All rights reserved. +// + +import Foundation + +struct ForecastTemperatureData: Decodable { + let maxTempC: Double + let maxTempF: Double + let minTempC: Double + let minTempF: Double + + private enum CodingKeys: String, CodingKey { + case maxTempC = "maxtemp_c" + case maxTempF = "maxtemp_f" + case minTempC = "mintemp_c" + case minTempF = "mintemp_f" + } +} diff --git a/DatWeatherDoe/API/Response/SunriseSunsetData.swift b/DatWeatherDoe/API/Response/SunriseSunsetData.swift new file mode 100644 index 0000000..2431396 --- /dev/null +++ b/DatWeatherDoe/API/Response/SunriseSunsetData.swift @@ -0,0 +1,24 @@ +// +// SunriseSunsetData.swift +// DatWeatherDoe +// +// Created by Inder Dhir on 6/22/24. +// Copyright © 2024 Inder Dhir. All rights reserved. +// + +import Foundation + +struct SunriseSunsetData: Decodable { + let isDay: Int + let sunrise: String + let sunset: String + + private enum CodingKeys: String, CodingKey { + case isDay = "is_sun_up" + case sunrise, sunset + } + + var isDayBool: Bool { + isDay > 0 + } +} diff --git a/DatWeatherDoe/API/Response/TemperatureData.swift b/DatWeatherDoe/API/Response/TemperatureData.swift new file mode 100644 index 0000000..bf68b25 --- /dev/null +++ b/DatWeatherDoe/API/Response/TemperatureData.swift @@ -0,0 +1,23 @@ +// +// TemperatureData.swift +// DatWeatherDoe +// +// Created by Inder Dhir on 6/23/24. +// Copyright © 2024 Inder Dhir. All rights reserved. +// + +import Foundation + +struct TemperatureData: Decodable { + let tempCelsius: Double + let feelsLikeTempCelsius: Double + let tempFahrenheit: Double + let feelsLikeTempFahrenheit: Double + + private enum CodingKeys: String, CodingKey { + case tempCelsius = "temp_c" + case feelsLikeTempCelsius = "feelslike_c" + case tempFahrenheit = "temp_f" + case feelsLikeTempFahrenheit = "feelslike_f" + } +} diff --git a/DatWeatherDoe/API/Response/WeatherAPIResponse.swift b/DatWeatherDoe/API/Response/WeatherAPIResponse.swift index 630dbca..e6d620a 100644 --- a/DatWeatherDoe/API/Response/WeatherAPIResponse.swift +++ b/DatWeatherDoe/API/Response/WeatherAPIResponse.swift @@ -9,77 +9,64 @@ import Foundation struct WeatherAPIResponse: Decodable { - let cityId: Int + let locationName: String let temperatureData: TemperatureData + let weatherConditionCode: Int let humidity: Int - let location: String - let weatherId: Int - let sunrise: TimeInterval - let sunset: TimeInterval let windData: WindData + let forecastDayData: ForecastDayData - struct TemperatureData: Decodable { - let temperature: Double - let feelsLikeTemperature: Double - let minTemperature: Double - let maxTemperature: Double - - // swiftlint:disable:next nesting - private enum CodingKeys: String, CodingKey { - case temperature = "temp" - case feelsLikeTemperature = "feels_like" - case minTemperature = "temp_min" - case maxTemperature = "temp_max" - } + private enum RootKeys: String, CodingKey { + case location, current, forecast } - struct WindData: Decodable { - let speed: Double - let degrees: Int - - // swiftlint:disable:next nesting - private enum CodingKeys: String, CodingKey { - case speed - case degrees = "deg" - } + private enum LocationKeys: String, CodingKey { + case name } - private enum RootKeys: String, CodingKey { - case cityId = "id" - case main, weather, humidity, name, sys, wind + private enum CurrentKeys: String, CodingKey { + case condition, humidity } - private enum MainKeys: String, CodingKey { - case humidity + private enum WeatherConditionKeys: String, CodingKey { + case code } - private enum SysKeys: String, CodingKey { - case sunrise, sunset + private enum ForecastKeys: String, CodingKey { + case forecastDay = "forecastday" } - private enum WeatherKeys: String, CodingKey { - case id + private enum ForecastDayKeys: String, CodingKey { + case day, astro } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: RootKeys.self) - cityId = try container.decode(Int.self, forKey: .cityId) - temperatureData = try container.decode(TemperatureData.self, forKey: .main) - - let mainContainer = try container.nestedContainer(keyedBy: MainKeys.self, forKey: .main) - humidity = try mainContainer.decode(Int.self, forKey: .humidity) - - location = try container.decode(String.self, forKey: .name) - - var weatherContainer = try container.nestedUnkeyedContainer(forKey: .weather) - let weatherChildContainer = try weatherContainer.nestedContainer(keyedBy: WeatherKeys.self) - weatherId = try weatherChildContainer.decode(Int.self, forKey: .id) - - let sysContainer = try container.nestedContainer(keyedBy: SysKeys.self, forKey: .sys) - sunrise = try sysContainer.decode(TimeInterval.self, forKey: .sunrise) - sunset = try sysContainer.decode(TimeInterval.self, forKey: .sunset) - - windData = try container.decode(WindData.self, forKey: .wind) + let locationContainer = try container.nestedContainer(keyedBy: LocationKeys.self, forKey: .location) + locationName = try locationContainer.decode(String.self, forKey: .name) + temperatureData = try container.decode(TemperatureData.self, forKey: .current) + + let currentContainer = try container.nestedContainer(keyedBy: CurrentKeys.self, forKey: .current) + let weatherConditionContainer = try currentContainer.nestedContainer( + keyedBy: WeatherConditionKeys.self, + forKey: .condition + ) + weatherConditionCode = try weatherConditionContainer.decode(Int.self, forKey: .code) + + windData = try container.decode(WindData.self, forKey: .current) + + humidity = try currentContainer.decode(Int.self, forKey: .humidity) + + let forecast = try container.decode(Forecast.self, forKey: .forecast) + if let dayData = forecast.dayDataArr.first { + forecastDayData = dayData + } else { + throw DecodingError.dataCorruptedError( + forKey: .forecast, + in: container, + debugDescription: "Missing forecast day data" + ) + } } } diff --git a/DatWeatherDoe/API/Response/WindData.swift b/DatWeatherDoe/API/Response/WindData.swift new file mode 100644 index 0000000..e44f5e7 --- /dev/null +++ b/DatWeatherDoe/API/Response/WindData.swift @@ -0,0 +1,23 @@ +// +// WindData.swift +// DatWeatherDoe +// +// Created by Inder Dhir on 6/23/24. +// Copyright © 2024 Inder Dhir. All rights reserved. +// + +import Foundation + +struct WindData: Decodable { + let speedMph: Double + let speedKph: Double + let degrees: Int + let direction: String + + private enum CodingKeys: String, CodingKey { + case speedMph = "wind_mph" + case speedKph = "wind_kph" + case degrees = "wind_degree" + case direction = "wind_dir" + } +} diff --git a/DatWeatherDoe/API/WeatherError.swift b/DatWeatherDoe/API/WeatherError.swift index 0892a90..4f877e8 100644 --- a/DatWeatherDoe/API/WeatherError.swift +++ b/DatWeatherDoe/API/WeatherError.swift @@ -12,8 +12,6 @@ enum WeatherError: LocalizedError { case unableToConstructUrl case locationError case latLongIncorrect - case zipCodeIncorrect - case cityIncorrect case networkError var errorDescription: String? { @@ -24,10 +22,6 @@ enum WeatherError: LocalizedError { return NSLocalizedString("❗️Location", comment: "Location error when fetching weather") case .latLongIncorrect: return NSLocalizedString("❗️Lat/Long", comment: "Lat/Long error when fetching weather") - case .zipCodeIncorrect: - return NSLocalizedString("❗️Zipcode", comment: "Zip Code error when fetching weather") - case .cityIncorrect: - return NSLocalizedString("❗️City", comment: "City error when fetching weather") case .networkError: return "🖧" } diff --git a/DatWeatherDoe/AppDelegate.swift b/DatWeatherDoe/AppDelegate.swift index 638e0a4..1fdc142 100644 --- a/DatWeatherDoe/AppDelegate.swift +++ b/DatWeatherDoe/AppDelegate.swift @@ -53,7 +53,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { viewModel = WeatherViewModel( locationFetcher: SystemLocationFetcher(logger: logger), weatherFactory: WeatherRepositoryFactory( - appId: WeatherAppIDParser().parse(), + appId: APIKeyParser().parse(), networkClient: NetworkClient(), logger: logger ), @@ -90,8 +90,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc private func terminate() { NSApp.terminate(self) } private func updateWeather(with weatherData: WeatherData) { - viewModel.updateCity(with: weatherData.response.cityId) - let measurementUnit = MeasurementUnit(rawValue: configManager.measurementUnit) ?? .imperial menuBarManager.updateMenuBarWith( weatherData: weatherData, diff --git a/DatWeatherDoe/Config/WeatherAppIDParser.swift b/DatWeatherDoe/Config/APIKeyParser.swift similarity index 60% rename from DatWeatherDoe/Config/WeatherAppIDParser.swift rename to DatWeatherDoe/Config/APIKeyParser.swift index daee8a7..cb29d6d 100644 --- a/DatWeatherDoe/Config/WeatherAppIDParser.swift +++ b/DatWeatherDoe/Config/APIKeyParser.swift @@ -1,5 +1,5 @@ // -// WeatherAppIDParser.swift +// APIKeyParser.swift // DatWeatherDoe // // Created by Inder Dhir on 1/11/22. @@ -8,11 +8,11 @@ import Foundation -final class WeatherAppIDParser { +final class APIKeyParser { func parse() -> String { - guard let appId = Bundle.main.infoDictionary?["OPENWEATHERMAP_APP_ID"] as? String else { + guard let apiKey = Bundle.main.infoDictionary?["WEATHER_API_KEY"] as? String else { fatalError("Unable to find OPENWEATHERMAP_APP_ID in `Config.xcconfig`") } - return appId + return apiKey } } diff --git a/DatWeatherDoe/Config/ConfigManager.swift b/DatWeatherDoe/Config/ConfigManager.swift index 352bf8d..2b2cc72 100644 --- a/DatWeatherDoe/Config/ConfigManager.swift +++ b/DatWeatherDoe/Config/ConfigManager.swift @@ -7,6 +7,7 @@ // import Foundation +import SwiftUI protocol ConfigManagerType: AnyObject { var measurementUnit: String { get set } @@ -27,44 +28,41 @@ protocol ConfigManagerType: AnyObject { } final class ConfigManager: ConfigManagerType { - @Storage(key: "measurementUnit", defaultValue: MeasurementUnit.imperial.rawValue) - public var measurementUnit: String + @AppStorage("measurementUnit") + public var measurementUnit = MeasurementUnit.imperial.rawValue - @Storage(key: "weatherSource", defaultValue: WeatherSource.location.rawValue) - public var weatherSource: String + @AppStorage("weatherSource") + public var weatherSource = WeatherSource.location.rawValue - @Storage(key: "weatherSourceText", defaultValue: nil) + @AppStorage("weatherSourceText") public var weatherSourceText: String? - @Storage(key: "refreshInterval", defaultValue: RefreshInterval.fifteenMinutes.rawValue) - public var refreshInterval: TimeInterval + @AppStorage("refreshInterval") + public var refreshInterval = RefreshInterval.fifteenMinutes.rawValue - @Storage(key: "isShowingWeatherIcon", defaultValue: true) - public var isShowingWeatherIcon: Bool + @AppStorage("isShowingWeatherIcon") + public var isShowingWeatherIcon = true - @Storage(key: "isShowingHumidity", defaultValue: false) - public var isShowingHumidity: Bool + @AppStorage("isShowingHumidity") + public var isShowingHumidity = false - @Storage(key: "isRoundingOffData", defaultValue: false) - public var isRoundingOffData: Bool + @AppStorage("isRoundingOffData") + public var isRoundingOffData = false - @Storage(key: "isUnitLetterOff", defaultValue: false) - public var isUnitLetterOff: Bool + @AppStorage("isUnitLetterOff") + public var isUnitLetterOff = false - @Storage(key: "isUnitSymbolOff", defaultValue: false) - public var isUnitSymbolOff: Bool + @AppStorage("isUnitSymbolOff") + public var isUnitSymbolOff = false - @Storage(key: "valueSeparator", defaultValue: "\u{007C}") - public var valueSeparator: String + @AppStorage("valueSeparator") + public var valueSeparator = "\u{007C}" - @Storage(key: "isWeatherConditionAsTextEnabled", defaultValue: false) - public var isWeatherConditionAsTextEnabled: Bool - - @Storage( - key: "weatherConditionPosition", - defaultValue: WeatherConditionPosition.beforeTemperature.rawValue - ) - public var weatherConditionPosition: String + @AppStorage("isWeatherConditionAsTextEnabled") + public var isWeatherConditionAsTextEnabled = false + + @AppStorage("weatherConditionPosition") + public var weatherConditionPosition = WeatherConditionPosition.beforeTemperature.rawValue func updateWeatherSource(_ source: WeatherSource, sourceText: String) { weatherSource = source.rawValue diff --git a/DatWeatherDoe/Config/Storage.swift b/DatWeatherDoe/Config/Storage.swift deleted file mode 100644 index 328c752..0000000 --- a/DatWeatherDoe/Config/Storage.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// Storage.swift -// DatWeatherDoe -// -// Created by Inder Dhir on 5/22/21. -// Copyright © 2021 Inder Dhir. All rights reserved. -// - -import Foundation - -@propertyWrapper -struct Storage { - private let key: String - private let defaultValue: T - private let storage: UserDefaults - - init( - key: String, - defaultValue: T, - storage: UserDefaults = .standard - ) { - self.key = key - self.defaultValue = defaultValue - self.storage = storage - } - - var wrappedValue: T { - get { storage.object(forKey: key) as? T ?? defaultValue } - set { - if let optional = newValue as? AnyOptional, optional.isNil { - storage.removeObject(forKey: key) - } else { - storage.set(newValue, forKey: key) - } - } - } -} - -private protocol AnyOptional { - var isNil: Bool { get } -} - -extension Optional: AnyOptional { - var isNil: Bool { self == nil } -} diff --git a/DatWeatherDoe/Resources/Info.plist b/DatWeatherDoe/Resources/Info.plist index ede8756..d06b354 100644 --- a/DatWeatherDoe/Resources/Info.plist +++ b/DatWeatherDoe/Resources/Info.plist @@ -2,6 +2,8 @@ + WEATHER_API_KEY + ${WEATHER_API_KEY} CFBundleDevelopmentRegion en CFBundleExecutable @@ -36,7 +38,5 @@ MainMenu NSPrincipalClass NSApplication - OPENWEATHERMAP_APP_ID - ${OPENWEATHERMAP_APP_ID} diff --git a/DatWeatherDoe/Resources/Localization/de.lproj/Localizable.strings b/DatWeatherDoe/Resources/Localization/de.lproj/Localizable.strings index 8c9c647..e2f0fee 100644 --- a/DatWeatherDoe/Resources/Localization/de.lproj/Localizable.strings +++ b/DatWeatherDoe/Resources/Localization/de.lproj/Localizable.strings @@ -13,9 +13,6 @@ /* Quit app */ "Quit" = "Beenden"; -/* 1 min refresh interval */ -"1 min" = "1 Min"; - /* 15 min refresh interval */ "15 min" = "15 Min"; @@ -31,9 +28,6 @@ /* Placeholder hint for entering Lat/Long */ "[latitude],[longitude]" = "[Breite],[Länge]"; -/* Placeholder hint for entering zip code */ -"[zipcode],[iso 3166 country code]" = "[PLZ],[ISO 3166 Länderkennung]"; - /* Show all temperature units */ "All" = "Alle"; @@ -49,36 +43,6 @@ /* Clear at night weather condition */ "Clear" = "Klar"; -/* Tornado weather condition */ -"Tornado" = "Tornado"; - -/* Squall weather condition */ -"Squall" = "Böen"; - -/* Ash weather condition */ -"Ash" = "Asche"; - -/* Dust weather condition */ -"Dust" = "Staub"; - -/* Sand weather condition */ -"Sand" = "Sand"; - -/* Fog weather condition */ -"Fog" = "Nebel"; - -/* Sand/Dust Whirls weather condition */ -"Sand/Dust Whirls" = "Sand-/Staubwirbel"; - -/* Haze weather condition */ -"Haze" = "Dunst"; - -/* Smoke weather condition */ -"Smoke" = "Rauch"; - -/* Mist weather condition */ -"Mist" = "Feuchter Nebel"; - /* Snow weather condition */ "Snow" = "Schnee"; @@ -144,9 +108,3 @@ /* Source for fetching weather */ "Weather Source" = "Wetterquelle"; - -/* Weather based on Zip Code */ -"Zipcode" = "Postleitzahl"; - -/* Zip Code error when fetching weather */ -"❗️Zipcode" = "Fehler bei Postleitzahl"; diff --git a/DatWeatherDoe/Resources/Localization/en.lproj/Localizable.strings b/DatWeatherDoe/Resources/Localization/en.lproj/Localizable.strings index 129abb1..ac69c3f 100644 --- a/DatWeatherDoe/Resources/Localization/en.lproj/Localizable.strings +++ b/DatWeatherDoe/Resources/Localization/en.lproj/Localizable.strings @@ -13,9 +13,6 @@ /* Quit app */ "Quit" = "Quit"; -/* 1 min refresh interval */ -"1 min" = "1 min"; - /* 15 min refresh interval */ "15 min" = "15 min"; @@ -31,9 +28,6 @@ /* Placeholder hint for entering Lat/Long */ "[latitude],[longitude]" = "[latitude],[longitude]"; -/* Placeholder hint for entering zip code */ -"[zipcode],[iso 3166 country code]" = "[zipcode],[iso 3166 country code]"; - /* Show all temperature units */ "All" = "All"; @@ -49,36 +43,6 @@ /* Clear at night weather condition */ "Clear" = "Clear"; -/* Tornado weather condition */ -"Tornado" = "Tornado"; - -/* Squall weather condition */ -"Squall" = "Squall"; - -/* Ash weather condition */ -"Ash" = "Ash"; - -/* Dust weather condition */ -"Dust" = "Dust"; - -/* Sand weather condition */ -"Sand" = "Sand"; - -/* Fog weather condition */ -"Fog" = "Fog"; - -/* Sand/Dust Whirls weather condition */ -"Sand/Dust Whirls" = "Sand/Dust Whirls"; - -/* Haze weather condition */ -"Haze" = "Haze"; - -/* Smoke weather condition */ -"Smoke" = "Smoke"; - -/* Mist weather condition */ -"Mist" = "Mist"; - /* Snow weather condition */ "Snow" = "Snow"; @@ -147,9 +111,3 @@ /* Source for fetching weather */ "Weather Source" = "Weather Source"; - -/* Weather based on Zip Code */ -"Zipcode" = "Zipcode"; - -/* Zip Code error when fetching weather */ -"❗️Zipcode" = "❗️Zipcode"; diff --git a/DatWeatherDoe/Resources/Localization/fr.lproj/Localizable.strings b/DatWeatherDoe/Resources/Localization/fr.lproj/Localizable.strings index 8f6a7d8..5237e85 100644 --- a/DatWeatherDoe/Resources/Localization/fr.lproj/Localizable.strings +++ b/DatWeatherDoe/Resources/Localization/fr.lproj/Localizable.strings @@ -16,12 +16,6 @@ /* Placeholder hint for entering Lat/Long */ "[latitude],[longitude]" = "[latitude],[longitude]"; -/* Placeholder hint for entering zip code */ -"[zipcode],[iso 3166 country code]" = "[code postal],[code pays iso 3166]"; - -/* 1 min refresh interval */ -"1 min" = "1 min"; - /* 5 min refresh interval */ "5 min" = "5 min"; @@ -40,33 +34,6 @@ /* Clear at night weather condition */ "Clear" = "Temps clair"; -/* Tornado weather condition */ -"Tornado" = "Tornade"; - -/* Squall weather condition */ -"Squall" = "Bourrasque"; - -/* Ash weather condition */ -"Ash" = "Cendre"; - -/* Dust weather condition */ -"Dust" = "Poussière"; - -/* Sand weather condition */ -"Sand" = "Sable"; - -/* Fog weather condition */ -"Fog" = "Brouillard"; - -/* Sand/Dust Whirls weather condition */ -"Sand/Dust Whirls" = "Tourbillons de sable/poussière"; - -/* Haze weather condition */ -"Haze" = "Brume"; - -/* Smoke weather condition */ -"Smoke" = "Fumée"; - /* Cloudy weather condition */ "Cloudy" = "Nuageux"; @@ -97,9 +64,6 @@ /* Location error when fetching weather */ "❗️Location" = "Erreur d’emplacement"; -/* Mist weather condition */ -"Mist" = "Brumeux"; - /* Partly cloudy weather condition */ "Partly cloudy" = "Partiellement couvert"; @@ -144,9 +108,3 @@ /* Source for fetching weather */ "Weather Source" = "Source de la météo"; - -/* Weather based on Zip Code */ -"Zip Code" = "Code postal"; - -/* Zip Code error when fetching weather */ -"❗️Zipcode" = "Erreur de code postal"; diff --git a/DatWeatherDoe/Resources/Localization/it.lproj/Localizable.strings b/DatWeatherDoe/Resources/Localization/it.lproj/Localizable.strings index c90ce50..33c2aa7 100644 --- a/DatWeatherDoe/Resources/Localization/it.lproj/Localizable.strings +++ b/DatWeatherDoe/Resources/Localization/it.lproj/Localizable.strings @@ -13,9 +13,6 @@ /* Quit app */ "Quit" = "Chiudi"; -/* 1 min refresh interval */ -"1 min" = "1 min"; - /* 15 min refresh interval */ "15 min" = "15 min"; @@ -31,9 +28,6 @@ /* Placeholder hint for entering Lat/Long */ "[latitude],[longitude]" = "[latitudine],[longitudine]"; -/* Placeholder hint for entering zip code */ -"[zipcode],[iso 3166 country code]" = "[codice CAP],[iso 3166 country code]"; - /* Show all temperature units */ "All" = "Entrambe"; @@ -49,36 +43,6 @@ /* Clear at night weather condition */ "Clear" = "Sereno"; -/* Tornado weather condition */ -"Tornado" = "Tornado"; - -/* Squall weather condition */ -"Squall" = "Acquazzone"; - -/* Ash weather condition */ -"Ash" = "Cenere"; - -/* Dust weather condition */ -"Dust" = "Polvere"; - -/* Sand weather condition */ -"Sand" = "Sabbia"; - -/* Fog weather condition */ -"Fog" = "Nebbia"; - -/* Sand/Dust Whirls weather condition */ -"Sand/Dust Whirls" = "Vortici Sabbia/Polvere"; - -/* Haze weather condition */ -"Haze" = "Caligine"; - -/* Smoke weather condition */ -"Smoke" = "Fumo"; - -/* Mist weather condition */ -"Mist" = "Foschia"; - /* Snow weather condition */ "Snow" = "Neve"; @@ -144,9 +108,3 @@ /* Source for fetching weather */ "Weather Source" = "Fonte Meteo"; - -/* Weather based on Zip Code */ -"Zipcode" = "Codice CAP"; - -/* Zip Code error when fetching weather */ -"❗️Zipcode" = "Errore Codice CAP"; diff --git a/DatWeatherDoe/Resources/Localization/ja.lproj/Localizable.strings b/DatWeatherDoe/Resources/Localization/ja.lproj/Localizable.strings index d2c7871..58d9742 100644 --- a/DatWeatherDoe/Resources/Localization/ja.lproj/Localizable.strings +++ b/DatWeatherDoe/Resources/Localization/ja.lproj/Localizable.strings @@ -13,9 +13,6 @@ /* Quit app */ "Quit" = "終了する"; -/* 1 min refresh interval */ -"1 min" = "1分"; - /* 15 min refresh interval */ "15 min" = "15分"; @@ -31,9 +28,6 @@ /* Placeholder hint for entering Lat/Long */ "[latitude],[longitude]" = "[緯度],[経度]"; -/* Placeholder hint for entering zip code */ -"[zipcode],[iso 3166 country code]" = "[郵便番号],[ISO 3166国名コード]"; - /* Show all temperature units */ "All" = "全て"; @@ -49,36 +43,6 @@ /* Clear at night weather condition */ "Clear" = "クリア"; -/* Tornado weather condition */ -"Tornado" = "竜巻"; - -/* Squall weather condition */ -"Squall" = "スコール"; - -/* Ash weather condition */ -"Ash" = "灰"; - -/* Dust weather condition */ -"Dust" = "埃"; - -/* Sand weather condition */ -"Sand" = "砂"; - -/* Fog weather condition */ -"Fog" = "霧"; - -/* Sand/Dust Whirls weather condition */ -"Sand/Dust Whirls" = "砂または埃の渦"; - -/* Haze weather condition */ -"Haze" = "霞"; - -/* Smoke weather condition */ -"Smoke" = "煙"; - -/* Mist weather condition */ -"Mist" = "靄"; - /* Snow weather condition */ "Snow" = "雪"; @@ -144,9 +108,3 @@ /* Source for fetching weather */ "Weather Source" = "気象情報源"; - -/* Weather based on Zip Code */ -"Zipcode" = "郵便番号"; - -/* Zip Code error when fetching weather */ -"❗️Zipcode" = "郵便番号のエラー"; diff --git a/DatWeatherDoe/Resources/Localization/zh-Hans.lproj/Localizable.strings b/DatWeatherDoe/Resources/Localization/zh-Hans.lproj/Localizable.strings index faa298a..2e064b9 100644 --- a/DatWeatherDoe/Resources/Localization/zh-Hans.lproj/Localizable.strings +++ b/DatWeatherDoe/Resources/Localization/zh-Hans.lproj/Localizable.strings @@ -13,9 +13,6 @@ /* Quit app */ "Quit" = "退出"; -/* 1 min refresh interval */ -"1 min" = "1分"; - /* 15 min refresh interval */ "15 min" = "15分"; @@ -31,12 +28,6 @@ /* Placeholder hint for entering Lat/Long */ "[latitude],[longitude]" = "[纬度],[经度]"; -/* Placeholder hint for entering zip code */ -"[zipcode],[iso 3166 country code]" = "[邮政编码],[ISO 3166国家代码]"; - -/* Placeholder hint for entering city */ -"[city],[iso 3166 country code]" = "[城市],[ISO 3166国家代码]"; - /* Show all temperature units */ "All" = "全部"; @@ -57,36 +48,6 @@ /* Clear at night weather condition */ "Clear" = "无云"; -/* Tornado weather condition */ -"Tornado" = "龙卷风"; - -/* Squall weather condition */ -"Squall" = "雷暴雨"; - -/* Ash weather condition */ -"Ash" = "火山灰天气"; - -/* Dust weather condition */ -"Dust" = "尘埃天气"; - -/* Sand weather condition */ -"Sand" = "沙尘"; - -/* Fog weather condition */ -"Fog" = "雾"; - -/* Sand/Dust Whirls weather condition */ -"Sand/Dust Whirls" = "局部龙卷风"; - -/* Haze weather condition */ -"Haze" = "霾"; - -/* Smoke weather condition */ -"Smoke" = "烟雾"; - -/* Mist weather condition */ -"Mist" = "薄雾"; - /* Snow weather condition */ "Snow" = "雪"; @@ -171,19 +132,6 @@ /* Source for fetching weather */ "Weather Source" = "天气数据来源"; -/* Weather based on Zip Code */ -"Zip Code" = "邮政编码"; - -/* Weather based on City */ -"City" = "城市"; - -/* Zip Code error when fetching weather */ -"❗️Zipcode" = "获取天气数据邮政编码错误"; - -/* City error when fetching weather */ -"❗️City" = "获取天气数据城市位置错误"; - - /*1、天气状况 AM Clouds / PM Sun=上午有云/下午后晴 AM Showers=上午阵雨 diff --git a/DatWeatherDoe/UI/Configure/ConfigureOptionsView.swift b/DatWeatherDoe/UI/Configure/ConfigureOptionsView.swift index c04acad..46b562c 100644 --- a/DatWeatherDoe/UI/Configure/ConfigureOptionsView.swift +++ b/DatWeatherDoe/UI/Configure/ConfigureOptionsView.swift @@ -30,7 +30,6 @@ struct ConfigureOptionsView: View { Text(LocalizedStringKey("Refresh Interval")) Spacer() Picker("", selection: $viewModel.refreshInterval) { - Text(LocalizedStringKey("1 min")).tag(RefreshInterval.oneMinute) Text(LocalizedStringKey("5 min")).tag(RefreshInterval.fiveMinutes) Text(LocalizedStringKey("15 min")).tag(RefreshInterval.fifteenMinutes) Text(LocalizedStringKey("30 min")).tag(RefreshInterval.thirtyMinutes) @@ -66,7 +65,7 @@ struct ConfigureOptionsView: View { Spacer() Toggle(isOn: $viewModel.isWeatherConditionAsTextEnabled) {} } - + HStack { Text(LocalizedStringKey("Weather Condition Position")) Spacer() diff --git a/DatWeatherDoe/UI/Configure/ConfigureView.swift b/DatWeatherDoe/UI/Configure/ConfigureView.swift index 3d8c4ec..2c9d8e5 100644 --- a/DatWeatherDoe/UI/Configure/ConfigureView.swift +++ b/DatWeatherDoe/UI/Configure/ConfigureView.swift @@ -19,7 +19,7 @@ struct ConfigureView: View { HStack { Spacer() .frame(alignment: .leading) - + Button(LocalizedStringKey("Done")) { viewModel.saveAndCloseConfig() } diff --git a/DatWeatherDoe/UI/Configure/ConfigureViewModel.swift b/DatWeatherDoe/UI/Configure/ConfigureViewModel.swift index 7cada71..2a08d28 100644 --- a/DatWeatherDoe/UI/Configure/ConfigureViewModel.swift +++ b/DatWeatherDoe/UI/Configure/ConfigureViewModel.swift @@ -53,7 +53,7 @@ final class ConfigureViewModel: ObservableObject { @Published var isWeatherConditionAsTextEnabled: Bool { didSet { configManager.isWeatherConditionAsTextEnabled = isWeatherConditionAsTextEnabled } } - + @Published var weatherConditionPosition: WeatherConditionPosition { didSet { configManager.weatherConditionPosition = weatherConditionPosition.rawValue } } @@ -75,7 +75,7 @@ final class ConfigureViewModel: ObservableObject { case 900: refreshInterval = .fifteenMinutes case 1800: refreshInterval = .thirtyMinutes case 3600: refreshInterval = .sixtyMinutes - default: refreshInterval = .oneMinute + default: refreshInterval = .fifteenMinutes } isShowingWeatherIcon = configManager.isShowingWeatherIcon @@ -85,11 +85,11 @@ final class ConfigureViewModel: ObservableObject { isUnitSymbolOff = configManager.isUnitSymbolOff isWeatherConditionAsTextEnabled = configManager.isWeatherConditionAsTextEnabled weatherConditionPosition = WeatherConditionPosition(rawValue: configManager.weatherConditionPosition) - ?? .beforeTemperature - + ?? .beforeTemperature + listenForConfigChange() } - + func saveConfig() { configManager.updateWeatherSource(weatherSource, sourceText: weatherSourceText) configManager.setConfigOptions( @@ -104,14 +104,14 @@ final class ConfigureViewModel: ObservableObject { ) ) } - + func saveAndCloseConfig() { saveConfig() popoverManager?.togglePopover(nil, shouldRefresh: hasConfigChanged) hasConfigChanged = false } - + private func listenForConfigChange() { objectWillChange.sink { [weak self] in self?.hasConfigChanged = true diff --git a/DatWeatherDoe/UI/Configure/ConfigureWeatherOptionsView.swift b/DatWeatherDoe/UI/Configure/ConfigureWeatherOptionsView.swift index 74d0301..7c929fb 100644 --- a/DatWeatherDoe/UI/Configure/ConfigureWeatherOptionsView.swift +++ b/DatWeatherDoe/UI/Configure/ConfigureWeatherOptionsView.swift @@ -19,8 +19,6 @@ struct ConfigureWeatherOptionsView: View { Picker("", selection: $viewModel.weatherSource) { Text(LocalizedStringKey("Location")).tag(WeatherSource.location) Text(LocalizedStringKey("Lat/Long")).tag(WeatherSource.latLong) - Text(LocalizedStringKey("Zip Code")).tag(WeatherSource.zipCode) - Text(LocalizedStringKey("City")).tag(WeatherSource.city) } .frame(width: 120) } diff --git a/DatWeatherDoe/UI/Configure/Options/RefreshInterval.swift b/DatWeatherDoe/UI/Configure/Options/RefreshInterval.swift index 5a9ecae..fc10242 100644 --- a/DatWeatherDoe/UI/Configure/Options/RefreshInterval.swift +++ b/DatWeatherDoe/UI/Configure/Options/RefreshInterval.swift @@ -9,7 +9,6 @@ import Foundation enum RefreshInterval: TimeInterval, CaseIterable, Identifiable { - case oneMinute = 60 case fiveMinutes = 300 case fifteenMinutes = 900 case thirtyMinutes = 1800 @@ -19,8 +18,6 @@ enum RefreshInterval: TimeInterval, CaseIterable, Identifiable { var title: String { switch self { - case .oneMinute: - return NSLocalizedString("1 min", comment: "1 min refresh interval") case .fiveMinutes: return NSLocalizedString("5 min", comment: "5 min refresh interval") case .fifteenMinutes: diff --git a/DatWeatherDoe/UI/Configure/Options/WeatherSource.swift b/DatWeatherDoe/UI/Configure/Options/WeatherSource.swift index 7272d02..689fcf9 100644 --- a/DatWeatherDoe/UI/Configure/Options/WeatherSource.swift +++ b/DatWeatherDoe/UI/Configure/Options/WeatherSource.swift @@ -9,7 +9,7 @@ import Foundation enum WeatherSource: String, CaseIterable { - case location, latLong, zipCode, city + case location, latLong var title: String { switch self { @@ -17,10 +17,6 @@ enum WeatherSource: String, CaseIterable { return NSLocalizedString("Location", comment: "Weather based on location") case .latLong: return NSLocalizedString("Lat/Long", comment: "Weather based on Lat/Long") - case .zipCode: - return NSLocalizedString("Zip Code", comment: "Weather based on Zip Code") - case .city: - return NSLocalizedString("City", comment: "Weather based on City") } } @@ -30,10 +26,6 @@ enum WeatherSource: String, CaseIterable { return "" case .latLong: return "42,42" - case .zipCode: - return "10021,us" - case .city: - return "Kyiv,ua" } } @@ -46,16 +38,6 @@ enum WeatherSource: String, CaseIterable { "[latitude],[longitude]", comment: "Placeholder hint for entering Lat/Long" ) - case .zipCode: - return NSLocalizedString( - "[zipcode],[iso 3166 country code]", - comment: "Placeholder hint for entering zip code" - ) - case .city: - return NSLocalizedString( - "[city],[iso 3166 country code]", - comment: "Placeholder hint for entering city" - ) } } } diff --git a/DatWeatherDoe/UI/Decorator/Condition/Builder/WeatherConditionBuilder.swift b/DatWeatherDoe/UI/Decorator/Condition/Builder/WeatherConditionBuilder.swift index 2cdfc4f..3bb8b7e 100644 --- a/DatWeatherDoe/UI/Decorator/Condition/Builder/WeatherConditionBuilder.swift +++ b/DatWeatherDoe/UI/Decorator/Condition/Builder/WeatherConditionBuilder.swift @@ -20,40 +20,35 @@ final class WeatherConditionBuilder: WeatherConditionBuilderType { } func build() -> WeatherCondition { - switch response.weatherId { - case 803 ... 804: + switch response.weatherConditionCode { + case 1006, 1009: return .cloudy - case 801 ... 802: - return isNight ? .partlyCloudyNight : .partlyCloudy - case 800: - return isNight ? .clearNight : .sunny - - case 701 ... 781: - return buildSmokyWeatherCondition() - - case 600 ... 622: + case 1003: + return isDay ? .partlyCloudy : .partlyCloudyNight + case 1000: + return isDay ? .sunny : .clearNight + case 1030: + return .mist + case 1135, 1147: + return .fog + case 1066, + 1114, 1117, + 1210, 1213, 1216, 1219, 1222, 1225, 1237, 1249, 1252, 1255, 1258, 1261, 1264, 1279, 1282: return .snow - - case 521 ... 531, 502 ... 504: + case 1192, 1195, 1243, 1246, 1276: return .heavyRain - case 511: + case 1069, 1072, 1168, 1171, 1198, 1201, 1204, 1207: return .freezingRain - case 500 ... 501, 520, 300 ... 321: - return isNight ? .lightRain : .partlyCloudyRain - - case 200 ... 232: + case 1063, 1150, 1153, 1180, 1183, 1186, 1189, 1240: + return isDay ? .partlyCloudyRain : .lightRain + case 1087, 1273: return .thunderstorm - default: - return WeatherCondition.getFallback(isNight: isNight) + return WeatherCondition.getFallback(isDay: isDay) } } - private var isNight: Bool { - Date().isNight(sunrise: response.sunrise, sunset: response.sunset) - } - - private func buildSmokyWeatherCondition() -> WeatherCondition { - WeatherSmokyConditionBuilder(response: response).build() + private var isDay: Bool { + response.forecastDayData.astro.isDayBool } } diff --git a/DatWeatherDoe/UI/Decorator/Condition/Builder/WeatherSmokyConditionBuilder.swift b/DatWeatherDoe/UI/Decorator/Condition/Builder/WeatherSmokyConditionBuilder.swift deleted file mode 100644 index d1da44b..0000000 --- a/DatWeatherDoe/UI/Decorator/Condition/Builder/WeatherSmokyConditionBuilder.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// WeatherSmokyConditionBuilder.swift -// DatWeatherDoe -// -// Created by Inder Dhir on 1/15/22. -// Copyright © 2022 Inder Dhir. All rights reserved. -// - -import Foundation - -protocol WeatherSmokyConditionBuilderType { - func build() -> WeatherCondition -} - -final class WeatherSmokyConditionBuilder: WeatherSmokyConditionBuilderType { - private let response: WeatherAPIResponse - - init(response: WeatherAPIResponse) { - self.response = response - } - - func build() -> WeatherCondition { - switch response.weatherId { - case 781: - return .smoky(condition: .tornado) - case 771: - return .smoky(condition: .squall) - case 762: - return .smoky(condition: .ash) - case 761: - return .smoky(condition: .dust) - case 751: - return .smoky(condition: .sand) - case 741: - return .smoky(condition: .fog) - case 731: - return .smoky(condition: .sandOrDustWhirls) - case 721: - return .smoky(condition: .haze) - case 711: - return .smoky(condition: .smoke) - case 701: - return .smoky(condition: .mist) - default: - return WeatherCondition.getFallback(isNight: isNight) - } - } - - private var isNight: Bool { - Date().isNight(sunrise: response.sunset, sunset: response.sunset) - } -} diff --git a/DatWeatherDoe/UI/Decorator/Condition/Date+Night.swift b/DatWeatherDoe/UI/Decorator/Condition/Date+Night.swift deleted file mode 100644 index 84ff6eb..0000000 --- a/DatWeatherDoe/UI/Decorator/Condition/Date+Night.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Date+Night.swift -// DatWeatherDoe -// -// Created by Inder Dhir on 1/15/22. -// Copyright © 2022 Inder Dhir. All rights reserved. -// - -import Foundation - -extension Date { - func isNight(sunrise: TimeInterval, sunset: TimeInterval) -> Bool { - timeIntervalSince1970 >= sunset || timeIntervalSince1970 <= sunrise - } -} diff --git a/DatWeatherDoe/UI/Decorator/Condition/Mapping/WeatherConditionImageMapper.swift b/DatWeatherDoe/UI/Decorator/Condition/Mapping/WeatherConditionImageMapper.swift index 4f3ac2b..c67b7df 100644 --- a/DatWeatherDoe/UI/Decorator/Condition/Mapping/WeatherConditionImageMapper.swift +++ b/DatWeatherDoe/UI/Decorator/Condition/Mapping/WeatherConditionImageMapper.swift @@ -25,6 +25,7 @@ final class WeatherConditionImageMapper: WeatherConditionImageMapperType { case .partlyCloudy: symbolName = "cloud.sun" accessibilityDescription = "Partly Cloudy" + case .partlyCloudyNight: symbolName = "cloud.moon" accessibilityDescription = "Partly Cloudy" @@ -32,13 +33,11 @@ final class WeatherConditionImageMapper: WeatherConditionImageMapperType { case .sunny: symbolName = "sun.max" accessibilityDescription = "Sunny" + case .clearNight: symbolName = "moon" accessibilityDescription = "Clear" - case let .smoky(smokyWeatherCondition): - return SmokyWeatherConditionImageMapper().map(smokyWeatherCondition) - case .snow: symbolName = "cloud.snow" accessibilityDescription = "Snow" @@ -54,36 +53,16 @@ final class WeatherConditionImageMapper: WeatherConditionImageMapperType { case .thunderstorm: symbolName = "cloud.bolt.rain" accessibilityDescription = "Thunderstorm" - } - - let config = NSImage.SymbolConfiguration(textStyle: .title2, scale: .medium) - return NSImage( - systemSymbolName: symbolName, - accessibilityDescription: accessibilityDescription - )?.withSymbolConfiguration(config) - } -} -private class SmokyWeatherConditionImageMapper { - func map(_ condition: SmokyWeatherCondition) -> NSImage? { - let symbolName: String - let accessibilityDescription: String - - switch condition { - case .tornado: - symbolName = "tornado" - accessibilityDescription = "Tornado" - case .squall: - symbolName = "wind" - accessibilityDescription = "Squall" - default: + case .mist, .fog: symbolName = "cloud.fog" accessibilityDescription = "Cloudy with Fog" } + let config = NSImage.SymbolConfiguration(textStyle: .title2, scale: .medium) return NSImage( systemSymbolName: symbolName, accessibilityDescription: accessibilityDescription - ) + )?.withSymbolConfiguration(config) } } diff --git a/DatWeatherDoe/UI/Decorator/Condition/Mapping/WeatherConditionTextMapper.swift b/DatWeatherDoe/UI/Decorator/Condition/Mapping/WeatherConditionTextMapper.swift index ab08d27..c30195f 100644 --- a/DatWeatherDoe/UI/Decorator/Condition/Mapping/WeatherConditionTextMapper.swift +++ b/DatWeatherDoe/UI/Decorator/Condition/Mapping/WeatherConditionTextMapper.swift @@ -23,12 +23,10 @@ final class WeatherConditionTextMapper: WeatherConditionTextMapperType { case .sunny: return NSLocalizedString("Sunny", comment: "Sunny weather condition") + case .clearNight: return NSLocalizedString("Clear", comment: "Clear at night weather condition") - case let .smoky(smokyWeatherCondition): - return SmokyWeatherConditionTextMapper().map(smokyWeatherCondition) - case .snow: return NSLocalizedString("Snow", comment: "Snow weather condition") @@ -40,6 +38,7 @@ final class WeatherConditionTextMapper: WeatherConditionTextMapperType { case .lightRain: return NSLocalizedString("Light rain", comment: "Light rain weather condition") + case .partlyCloudyRain: return NSLocalizedString( "Partly cloudy with rain", @@ -48,33 +47,12 @@ final class WeatherConditionTextMapper: WeatherConditionTextMapperType { case .thunderstorm: return NSLocalizedString("Thunderstorm", comment: "Thunderstorm weather condition") - } - } -} -private class SmokyWeatherConditionTextMapper { - func map(_ smokyWeatherCondition: SmokyWeatherCondition) -> String { - switch smokyWeatherCondition { - case .tornado: - return NSLocalizedString("Tornado", comment: "Tornado weather condition") - case .squall: - return NSLocalizedString("Squall", comment: "Squall weather condition") - case .ash: - return NSLocalizedString("Ash", comment: "Ash weather condition") - case .dust: - return NSLocalizedString("Dust", comment: "Dust weather condition") - case .sand: - return NSLocalizedString("Sand", comment: "Sand weather condition") - case .fog: - return NSLocalizedString("Fog", comment: "Fog weather condition") - case .sandOrDustWhirls: - return NSLocalizedString("Sand/Dust Whirls", comment: "Sand/Dust Whirls weather condition") - case .haze: - return NSLocalizedString("Haze", comment: "Haze weather condition") - case .smoke: - return NSLocalizedString("Smoke", comment: "Smoke weather condition") case .mist: return NSLocalizedString("Mist", comment: "Mist weather condition") + + case .fog: + return NSLocalizedString("Fog", comment: "Fog weather condition") } } } diff --git a/DatWeatherDoe/UI/Decorator/Condition/SmokyWeatherCondition.swift b/DatWeatherDoe/UI/Decorator/Condition/SmokyWeatherCondition.swift deleted file mode 100644 index 874a357..0000000 --- a/DatWeatherDoe/UI/Decorator/Condition/SmokyWeatherCondition.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// SmokyWeatherCondition.swift -// DatWeatherDoe -// -// Created by Inder Dhir on 1/9/22. -// Copyright © 2022 Inder Dhir. All rights reserved. -// - -import AppKit -import Foundation - -enum SmokyWeatherCondition: String { - case tornado = "Tornado" - case squall = "Squall" - case ash = "Ash" - case dust = "Dust" - case sand = "Sand" - case fog = "Fog" - case sandOrDustWhirls = "Sand/Dust Whirls" - case haze = "Haze" - case smoke = "Smoke" - case mist = "Mist" -} diff --git a/DatWeatherDoe/UI/Decorator/Condition/WeatherCondition.swift b/DatWeatherDoe/UI/Decorator/Condition/WeatherCondition.swift index e64aa3b..48293d9 100644 --- a/DatWeatherDoe/UI/Decorator/Condition/WeatherCondition.swift +++ b/DatWeatherDoe/UI/Decorator/Condition/WeatherCondition.swift @@ -10,23 +10,14 @@ import AppKit import Foundation enum WeatherCondition { - case cloudy - - case partlyCloudy, partlyCloudyNight + case cloudy, partlyCloudy, partlyCloudyNight case sunny, clearNight - - case smoky(condition: SmokyWeatherCondition) - case snow - - case heavyRain - case freezingRain - case lightRain - case partlyCloudyRain - + case heavyRain, freezingRain, lightRain, partlyCloudyRain case thunderstorm + case mist, fog - static func getFallback(isNight: Bool) -> WeatherCondition { - isNight ? .clearNight : .sunny + static func getFallback(isDay: Bool) -> WeatherCondition { + isDay ? .sunny : .clearNight } } diff --git a/DatWeatherDoe/UI/Decorator/Text/SunriseAndSunsetTextBuilder.swift b/DatWeatherDoe/UI/Decorator/Text/SunriseAndSunsetTextBuilder.swift index 0390a1d..f1a4cbf 100644 --- a/DatWeatherDoe/UI/Decorator/Text/SunriseAndSunsetTextBuilder.swift +++ b/DatWeatherDoe/UI/Decorator/Text/SunriseAndSunsetTextBuilder.swift @@ -13,34 +13,18 @@ protocol SunriseAndSunsetTextBuilderType { } final class SunriseAndSunsetTextBuilder: SunriseAndSunsetTextBuilderType { - private let sunset: TimeInterval - private let sunrise: TimeInterval + private let sunset: String + private let sunrise: String private let upArrowStr = "⬆" private let downArrowStr = "⬇" - init( - sunset: TimeInterval, - sunrise: TimeInterval - ) { + init(sunset: String, sunrise: String) { self.sunset = sunset self.sunrise = sunrise } func build() -> String { - buildRiseSet() - } - - private func buildRiseSet() -> String { - let sunRiseText = buildFormattedString(timestamp: sunrise) - let sunSetText = buildFormattedString(timestamp: sunset) - let sunriseAndSunsetString = "\(upArrowStr)\(sunRiseText) \(downArrowStr)\(sunSetText)" - return sunriseAndSunsetString - } - - private func buildFormattedString(timestamp: TimeInterval) -> String { - let formatter = DateFormatter() - formatter.timeStyle = .short - return formatter.string(from: Date(timeIntervalSince1970: timestamp)) + "\(upArrowStr)\(sunrise) \(downArrowStr)\(sunset)" } } diff --git a/DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureConverter.swift b/DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureConverter.swift deleted file mode 100644 index 1aa4ebe..0000000 --- a/DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureConverter.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// TemperatureConverter.swift -// DatWeatherDoe -// -// Created by Inder Dhir on 1/13/22. -// Copyright © 2022 Inder Dhir. All rights reserved. -// - -protocol TemperatureConverterType { - func convertKelvinToFahrenheit(_ kelvinTemperature: Double) -> Double - func convertKelvinToCelsius(_ kelvinTemperature: Double) -> Double -} - -final class TemperatureConverter: TemperatureConverterType { - func convertKelvinToFahrenheit(_ kelvinTemperature: Double) -> Double { - kelvinTemperature.fahrenheitTemperature - } - - func convertKelvinToCelsius(_ kelvinTemperature: Double) -> Double { - kelvinTemperature.celsiusTemperature - } -} - -private extension Double { - var fahrenheitTemperature: Double { ((self - 273.15) * 1.8) + 32 } - var celsiusTemperature: Double { self - 273.15 } -} diff --git a/DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureForecastTextBuilder.swift b/DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureForecastTextBuilder.swift index 5ae14d2..975e42e 100644 --- a/DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureForecastTextBuilder.swift +++ b/DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureForecastTextBuilder.swift @@ -13,17 +13,20 @@ protocol TemperatureForecastTextBuilderType { } final class TemperatureForecastTextBuilder: TemperatureForecastTextBuilderType { - private let temperatureData: WeatherAPIResponse.TemperatureData + private let temperatureData: TemperatureData + private let forecastTemperatureData: ForecastTemperatureData private let options: TemperatureTextBuilder.Options private let upArrowStr = "⬆" private let downArrowStr = "⬇" private let degreeString = "\u{00B0}" init( - temperatureData: WeatherAPIResponse.TemperatureData, + temperatureData: TemperatureData, + forecastTemperatureData: ForecastTemperatureData, options: TemperatureTextBuilder.Options ) { self.temperatureData = temperatureData + self.forecastTemperatureData = forecastTemperatureData self.options = options } @@ -37,20 +40,20 @@ final class TemperatureForecastTextBuilder: TemperatureForecastTextBuilderType { private func buildTemperatureForAllUnits() -> String { let feelsLikeTempFahrenheit = buildFormattedTemperature( - temperatureData.feelsLikeTemperature, unit: .fahrenheit + temperatureData.feelsLikeTempFahrenheit, unit: .fahrenheit ) let feelsLikeTempCelsius = buildFormattedTemperature( - temperatureData.feelsLikeTemperature, unit: .celsius + temperatureData.feelsLikeTempCelsius, unit: .celsius ) let feelsLikeTemperatureCombined = [feelsLikeTempFahrenheit, feelsLikeTempCelsius] .compactMap { $0 } .joined(separator: " / ") let maxTempFahrenheit = buildFormattedTemperature( - temperatureData.maxTemperature, unit: .fahrenheit + forecastTemperatureData.maxTempF, unit: .fahrenheit ) let maxTempCelsius = buildFormattedTemperature( - temperatureData.maxTemperature, unit: .celsius + forecastTemperatureData.maxTempC, unit: .celsius ) let maxTempCombined = [maxTempFahrenheit, maxTempCelsius] .compactMap { $0 } @@ -60,10 +63,10 @@ final class TemperatureForecastTextBuilder: TemperatureForecastTextBuilderType { .joined() let minTempFahrenheit = buildFormattedTemperature( - temperatureData.minTemperature, unit: .fahrenheit + forecastTemperatureData.minTempF, unit: .fahrenheit ) let minTempCelsius = buildFormattedTemperature( - temperatureData.minTemperature, unit: .celsius + forecastTemperatureData.minTempC, unit: .celsius ) let minTempCombined = [minTempFahrenheit, minTempCelsius] .compactMap { $0 } @@ -81,36 +84,35 @@ final class TemperatureForecastTextBuilder: TemperatureForecastTextBuilderType { } private func buildTemperatureForUnit(_ unit: TemperatureUnit) -> String { - let maxTemperature = buildFormattedTemperature(temperatureData.maxTemperature, unit: unit) - let maxTempStr = [upArrowStr, maxTemperature] + let maxTemp = unit == .fahrenheit ? forecastTemperatureData.maxTempF : forecastTemperatureData.maxTempC + let formattedMaxTemp = buildFormattedTemperature(maxTemp, unit: unit) + let maxTempStr = [upArrowStr, formattedMaxTemp] .compactMap { $0 } .joined() - let minTemperature = buildFormattedTemperature(temperatureData.minTemperature, unit: unit) - let minTempStr = [downArrowStr, minTemperature] + let minTemp = unit == .fahrenheit ? forecastTemperatureData.minTempF : forecastTemperatureData.minTempC + let formatedMinTemp = buildFormattedTemperature(minTemp, unit: unit) + let minTempStr = [downArrowStr, formatedMinTemp] .compactMap { $0 } .joined() let maxAndMinTempStr = [maxTempStr, minTempStr] .compactMap { $0 } .joined(separator: " ") - - let feelsLikeTemperature = buildFormattedTemperature(temperatureData.feelsLikeTemperature, unit: unit) - return [feelsLikeTemperature, maxAndMinTempStr] + + let feelsLikeTemp = unit == .fahrenheit ? + temperatureData.feelsLikeTempFahrenheit : + temperatureData.feelsLikeTempCelsius + let formattedFeelsLikeTemp = buildFormattedTemperature(feelsLikeTemp, unit: unit) + return [formattedFeelsLikeTemp, maxAndMinTempStr] .compactMap { $0 } .joined(separator: " - ") } private func buildFormattedTemperature( - _ kelvinTemperature: Double, + _ temperatureForUnit: Double, unit: TemperatureUnit ) -> String? { - let temperatureForUnit = if unit == .fahrenheit { - TemperatureConverter().convertKelvinToFahrenheit(kelvinTemperature) - } else { - TemperatureConverter().convertKelvinToCelsius(kelvinTemperature) - } - guard let temperatureString = TemperatureFormatter() .getFormattedTemperatureString(temperatureForUnit, isRoundingOff: options.isRoundingOff) else { diff --git a/DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureTextBuilder.swift b/DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureTextBuilder.swift index 6c4656d..e8b9e72 100644 --- a/DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureTextBuilder.swift +++ b/DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureTextBuilder.swift @@ -42,12 +42,12 @@ final class TemperatureTextBuilder: TemperatureTextBuilderType { } private func buildTemperatureTextForAllUnits() -> String? { - let fahrenheitTemperature = buildTemperature(isFahrenheit: true) - let celsiusTemperature = buildTemperature(isFahrenheit: false) - let temperatureWithDegrees = temperatureCreator.getTemperatureWithDegrees( temperatureInMultipleUnits: - .init(fahrenheit: fahrenheitTemperature, celsius: celsiusTemperature), + .init( + fahrenheit: response.temperatureData.tempFahrenheit, + celsius: response.temperatureData.tempCelsius + ), isRoundingOff: options.isRoundingOff, isUnitLetterOff: options.isUnitLetterOff, isUnitSymbolOff: options.isUnitSymbolOff @@ -56,7 +56,9 @@ final class TemperatureTextBuilder: TemperatureTextBuilderType { } private func buildTemperatureText(for unit: TemperatureUnit) -> String? { - let temperatureForUnit = buildTemperature(isFahrenheit: unit == .fahrenheit) + let temperatureForUnit = unit == .fahrenheit ? + response.temperatureData.tempFahrenheit : + response.temperatureData.tempCelsius let temperatureWithDegrees = temperatureCreator.getTemperatureWithDegrees( temperatureForUnit, unit: unit, @@ -66,12 +68,4 @@ final class TemperatureTextBuilder: TemperatureTextBuilderType { ) return temperatureWithDegrees } - - private func buildTemperature(isFahrenheit: Bool) -> Double { - if isFahrenheit { - TemperatureConverter().convertKelvinToFahrenheit(response.temperatureData.temperature) - } else { - TemperatureConverter().convertKelvinToCelsius(response.temperatureData.temperature) - } - } } diff --git a/DatWeatherDoe/UI/Decorator/Text/WeatherTextBuilder.swift b/DatWeatherDoe/UI/Decorator/Text/WeatherTextBuilder.swift index 1895669..f1c2728 100644 --- a/DatWeatherDoe/UI/Decorator/Text/WeatherTextBuilder.swift +++ b/DatWeatherDoe/UI/Decorator/Text/WeatherTextBuilder.swift @@ -41,7 +41,7 @@ final class WeatherTextBuilder: WeatherTextBuilderType { buildWeatherConditionAsText return finalString } - + private func appendTemperatureAsText() -> String { TemperatureTextBuilder( response: response, @@ -60,17 +60,17 @@ final class WeatherTextBuilder: WeatherTextBuilderType { logger: logger ).build() } - + private func buildWeatherConditionAsText(initial: String) -> String { guard options.isWeatherConditionAsTextEnabled else { return initial } let weatherCondition = WeatherConditionBuilder(response: response).build() let weatherConditionText = WeatherConditionTextMapper().map(weatherCondition) - + let combinedString = options.conditionPosition == .beforeTemperature ? - [weatherConditionText, initial] : - [initial, weatherConditionText.lowercased()] - + [weatherConditionText, initial] : + [initial, weatherConditionText.lowercased()] + return combinedString .compactMap { $0 } .joined(separator: ", ") diff --git a/DatWeatherDoe/UI/Forecaster/WeatherForecaster.swift b/DatWeatherDoe/UI/Forecaster/WeatherForecaster.swift index 6c49fd5..bbbd6e8 100644 --- a/DatWeatherDoe/UI/Forecaster/WeatherForecaster.swift +++ b/DatWeatherDoe/UI/Forecaster/WeatherForecaster.swift @@ -10,20 +10,13 @@ import Cocoa import Foundation protocol WeatherForecasterType { - func updateCityWith(cityId: Int) func seeForecastForCity() } final class WeatherForecaster: WeatherForecasterType { - private let fullWeatherUrl = URL(string: "https://openweathermap.org/city")! - private var cityId = 0 - - func updateCityWith(cityId: Int) { - self.cityId = cityId - } + private let fullWeatherUrl = URL(string: "https://www.weatherapi.com/weather/")! func seeForecastForCity() { - let cityWeatherUrl = fullWeatherUrl.appendingPathComponent(String(cityId)) - NSWorkspace.shared.open(cityWeatherUrl) + NSWorkspace.shared.open(fullWeatherUrl) } } diff --git a/DatWeatherDoe/UI/Menu Bar/Popover/PopoverManager.swift b/DatWeatherDoe/UI/Menu Bar/Popover/PopoverManager.swift index 53ad73e..f370dba 100644 --- a/DatWeatherDoe/UI/Menu Bar/Popover/PopoverManager.swift +++ b/DatWeatherDoe/UI/Menu Bar/Popover/PopoverManager.swift @@ -26,14 +26,14 @@ final class PopoverManager { eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: mouseEventHandler) configureViewModel = ConfigureViewModel(configManager: configManager, popoverManager: self) - - setupConfigurationView(configManager) + + setupConfigurationView() } func togglePopover(_ sender: AnyObject?, shouldRefresh: Bool) { if popover.isShown { closePopover(sender) - + if shouldRefresh { refreshCallback() } @@ -42,9 +42,9 @@ final class PopoverManager { } } - private func setupConfigurationView(_ configManager: ConfigManagerType) { + private func setupConfigurationView() { popover.contentSize = NSSize(width: 360, height: 360) - + let version = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "1.0.0" popover.contentViewController = NSHostingController( rootView: ConfigureView(viewModel: configureViewModel, version: version) @@ -58,13 +58,13 @@ final class PopoverManager { eventMonitor?.start() } } - + private func closePopover(_ sender: AnyObject?) { popover.performClose(sender) eventMonitor?.stop() } - - private func mouseEventHandler(_ event: NSEvent?) { + + private func mouseEventHandler(_: NSEvent?) { if popover.isShown { closePopover(nil) } diff --git a/DatWeatherDoe/UI/Menu Bar/StatusItem/StatusItemManager.swift b/DatWeatherDoe/UI/Menu Bar/StatusItem/StatusItemManager.swift index e3e29f4..bdeb2c5 100644 --- a/DatWeatherDoe/UI/Menu Bar/StatusItem/StatusItemManager.swift +++ b/DatWeatherDoe/UI/Menu Bar/StatusItem/StatusItemManager.swift @@ -41,6 +41,7 @@ final class StatusItemManager { if let textualRepresentation = weatherData.textualRepresentation { self.statusItem.button?.title = textualRepresentation } + if weatherData.showWeatherIcon { self.statusItem.button?.image = self.getImageFrom(weatherData: weatherData) self.statusItem.button?.imagePosition = .imageLeading @@ -124,12 +125,13 @@ final class StatusItemManager { } private func getLocationFrom(weatherData: WeatherData) -> String { - weatherData.response.location + weatherData.response.locationName } private func getWeatherTextFrom(weatherData: WeatherData, options: Options) -> String { TemperatureForecastTextBuilder( temperatureData: weatherData.response.temperatureData, + forecastTemperatureData: weatherData.response.forecastDayData.temp, options: .init( unit: options.unit.temperatureUnit, isRoundingOff: options.isRoundingOff, @@ -141,8 +143,8 @@ final class StatusItemManager { private func getSunRiseSetFrom(weatherData: WeatherData) -> String { SunriseAndSunsetTextBuilder( - sunset: weatherData.response.sunset, - sunrise: weatherData.response.sunrise + sunset: weatherData.response.forecastDayData.astro.sunset, + sunrise: weatherData.response.forecastDayData.astro.sunrise ).build() } @@ -150,7 +152,7 @@ final class StatusItemManager { WeatherConditionTextMapper().map(weatherData.weatherCondition) } - private func getWindSpeedItemFrom(data: WeatherAPIResponse.WindData, options: Options) -> String { + private func getWindSpeedItemFrom(data: WindData, options: Options) -> String { if options.unit == .all { WindSpeedFormatter() .getFormattedWindSpeedStringForAllUnits( diff --git a/DatWeatherDoe/UI/Menu Bar/StatusItem/WindSpeedFormatter.swift b/DatWeatherDoe/UI/Menu Bar/StatusItem/WindSpeedFormatter.swift index 00bf457..d2df64a 100644 --- a/DatWeatherDoe/UI/Menu Bar/StatusItem/WindSpeedFormatter.swift +++ b/DatWeatherDoe/UI/Menu Bar/StatusItem/WindSpeedFormatter.swift @@ -15,13 +15,13 @@ struct WindSpeedInMultipleUnits { protocol WindSpeedFormatterType { func getFormattedWindSpeedStringForAllUnits( - windData: WeatherAPIResponse.WindData, + windData: WindData, isRoundingOff: Bool ) -> String func getFormattedWindSpeedString( unit: MeasurementUnit, - windData: WeatherAPIResponse.WindData + windData: WindData ) -> String } @@ -37,12 +37,12 @@ final class WindSpeedFormatter: WindSpeedFormatterType { }() func getFormattedWindSpeedStringForAllUnits( - windData: WeatherAPIResponse.WindData, + windData: WindData, isRoundingOff _: Bool ) -> String { - let mpsSpeed = windData.speed + let mphSpeed = windData.speedMph + let mpsSpeed = mpsSpeedFrom(mphSpeed: mphSpeed) - let mphSpeed = 2.236 * mpsSpeed let mphRounded = formatter.string(from: NSNumber(value: mphSpeed)) ?? "" let windSpeedMph = [mphRounded, "mi/hr"].joined() @@ -56,17 +56,21 @@ final class WindSpeedFormatter: WindSpeedFormatterType { func getFormattedWindSpeedString( unit: MeasurementUnit, - windData: WeatherAPIResponse.WindData + windData: WindData ) -> String { - let windSpeedRounded = formatter.string(from: NSNumber(value: windData.speed)) ?? "" + let mphSpeed = windData.speedMph + let mpsSpeed = mpsSpeedFrom(mphSpeed: mphSpeed) + + let speed = unit == .imperial ? mphSpeed : mpsSpeed + let speedRounded = formatter.string(from: NSNumber(value: speed)) ?? "" let windSpeedSuffix = unit == .imperial ? "mi/hr" : "m/s" - let windSpeedStr = [windSpeedRounded, windSpeedSuffix].joined() + let windSpeedStr = [speedRounded, windSpeedSuffix].joined() return combinedWindString(windData: windData, windSpeed: windSpeedStr) } private func combinedWindString( - windData: WeatherAPIResponse.WindData, + windData: WindData, windSpeed: String ) -> String { let windDegreesStr = [String(windData.degrees), degreeString].joined() @@ -79,4 +83,8 @@ final class WindSpeedFormatter: WindSpeedFormatterType { let windFullStr = [windAndDegreesStr, windDirectionStr].joined(separator: " ") return windFullStr } + + private func mpsSpeedFrom(mphSpeed: Double) -> Double { + 0.4469 * mphSpeed + } } diff --git a/DatWeatherDoe/ViewModel/Repository/City/CityValidator.swift b/DatWeatherDoe/ViewModel/Repository/City/CityValidator.swift deleted file mode 100644 index 5e282f2..0000000 --- a/DatWeatherDoe/ViewModel/Repository/City/CityValidator.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// CityValidator.swift -// DatWeatherDoe -// -// Created by preckrasno on 14.02.2023. -// Copyright © 2023 Inder Dhir. All rights reserved. -// - -import Foundation - -final class CityValidator: WeatherValidatorType { - private let city: String - - init(city: String) { - self.city = city - } - - func validate() throws { - let isCityPresent = !city.isEmpty - let isCityPresentWithCountryCode = city.split(separator: ",").count == 2 - let isValid = isCityPresent && isCityPresentWithCountryCode - if !isValid { - throw WeatherError.cityIncorrect - } - } -} diff --git a/DatWeatherDoe/ViewModel/Repository/City/CityWeatherRepository.swift b/DatWeatherDoe/ViewModel/Repository/City/CityWeatherRepository.swift deleted file mode 100644 index 9609957..0000000 --- a/DatWeatherDoe/ViewModel/Repository/City/CityWeatherRepository.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// CityWeatherRepository.swift -// DatWeatherDoe -// -// Created by preckrasno on 14.02.2023. -// Copyright © 2023 Inder Dhir. All rights reserved. -// - -import Foundation -import OSLog - -final class CityWeatherRepository: WeatherRepositoryType { - private let appId: String - private let city: String - private let networkClient: NetworkClientType - private let logger: Logger - - init( - appId: String, - city: String, - networkClient: NetworkClientType, - logger: Logger - ) { - self.appId = appId - self.city = city - self.networkClient = networkClient - self.logger = logger - } - - func getWeather() async throws -> WeatherAPIResponse { - logger.debug("Getting weather via city") - - do { - try CityValidator(city: city).validate() - let url = try CityWeatherURLBuilder(appId: appId, city: city).build() - let data = try await networkClient.performRequest(url: url) - return try WeatherAPIResponseParser().parse(data) - } catch { - logger.error("Getting weather via city failed.") - - throw error - } - } -} diff --git a/DatWeatherDoe/ViewModel/Repository/City/CityWeatherURLBuilder.swift b/DatWeatherDoe/ViewModel/Repository/City/CityWeatherURLBuilder.swift deleted file mode 100644 index 889b539..0000000 --- a/DatWeatherDoe/ViewModel/Repository/City/CityWeatherURLBuilder.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// CityWeatherURLBuilder.swift -// DatWeatherDoe -// -// Created by preckrasno on 14.02.2023. -// Copyright © 2023 Inder Dhir. All rights reserved. -// - -import Foundation - -final class CityWeatherURLBuilder: WeatherURLBuilder { - private let city: String - - init(appId: String, city: String) { - self.city = city - super.init(appId: appId) - } - - override func build() throws -> URL { - let queryItems: [URLQueryItem] = [ - URLQueryItem(name: "appid", value: appId), - URLQueryItem(name: "q", value: city) - ] - - var urlComps = URLComponents(string: apiUrlString) - urlComps?.queryItems = queryItems - guard let finalUrl = urlComps?.url else { - throw WeatherError.unableToConstructUrl - } - return finalUrl - } -} diff --git a/DatWeatherDoe/ViewModel/Repository/Location/Coordinates/LocationCoordinatesWeatherRepository.swift b/DatWeatherDoe/ViewModel/Repository/Location/Coordinates/LocationCoordinatesWeatherRepository.swift index 7e05bc5..7a42d50 100644 --- a/DatWeatherDoe/ViewModel/Repository/Location/Coordinates/LocationCoordinatesWeatherRepository.swift +++ b/DatWeatherDoe/ViewModel/Repository/Location/Coordinates/LocationCoordinatesWeatherRepository.swift @@ -32,7 +32,7 @@ final class LocationCoordinatesWeatherRepository: WeatherRepositoryType { do { let location = try getLocationCoordinatesFrom(latLong) - let url = try LocationWeatherURLBuilder(appId: appId, location: location).build() + let url = try WeatherURLBuilder(appId: appId, location: location).build() let data = try await networkClient.performRequest(url: url) return try WeatherAPIResponseParser().parse(data) } catch { diff --git a/DatWeatherDoe/ViewModel/Repository/Location/LocationWeatherURLBuilder.swift b/DatWeatherDoe/ViewModel/Repository/Location/LocationWeatherURLBuilder.swift deleted file mode 100644 index b72b8a8..0000000 --- a/DatWeatherDoe/ViewModel/Repository/Location/LocationWeatherURLBuilder.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// LocationWeatherURLBuilder.swift -// DatWeatherDoe -// -// Created by Inder Dhir on 1/16/22. -// Copyright © 2022 Inder Dhir. All rights reserved. -// - -import CoreLocation - -final class LocationWeatherURLBuilder: WeatherURLBuilder { - private let location: CLLocationCoordinate2D - - init(appId: String, location: CLLocationCoordinate2D) { - self.location = location - super.init(appId: appId) - } - - override func build() throws -> URL { - let queryItems: [URLQueryItem] = [ - URLQueryItem(name: "appid", value: appId), - URLQueryItem(name: "lat", value: String(describing: location.latitude)), - URLQueryItem(name: "lon", value: String(describing: location.longitude)) - ] - - var urlComps = URLComponents(string: apiUrlString) - urlComps?.queryItems = queryItems - - guard let finalUrl = urlComps?.url else { - throw WeatherError.unableToConstructUrl - } - return finalUrl - } -} diff --git a/DatWeatherDoe/ViewModel/Repository/Location/System/SystemLocationWeatherRepository.swift b/DatWeatherDoe/ViewModel/Repository/Location/System/SystemLocationWeatherRepository.swift index 146789e..d280a18 100644 --- a/DatWeatherDoe/ViewModel/Repository/Location/System/SystemLocationWeatherRepository.swift +++ b/DatWeatherDoe/ViewModel/Repository/Location/System/SystemLocationWeatherRepository.swift @@ -31,7 +31,7 @@ final class SystemLocationWeatherRepository: WeatherRepositoryType { logger.debug("Getting weather via location") do { - let url = try LocationWeatherURLBuilder(appId: appId, location: location).build() + let url = try WeatherURLBuilder(appId: appId, location: location).build() let data = try await networkClient.performRequest(url: url) return try WeatherAPIResponseParser().parse(data) } catch { diff --git a/DatWeatherDoe/ViewModel/Repository/WeatherRepositoryFactory.swift b/DatWeatherDoe/ViewModel/Repository/WeatherRepositoryFactory.swift index aff9e07..b70414b 100644 --- a/DatWeatherDoe/ViewModel/Repository/WeatherRepositoryFactory.swift +++ b/DatWeatherDoe/ViewModel/Repository/WeatherRepositoryFactory.swift @@ -13,8 +13,6 @@ import OSLog protocol WeatherRepositoryFactoryType { func create(location: CLLocationCoordinate2D) -> WeatherRepositoryType func create(latLong: String) -> WeatherRepositoryType - func create(zipCode: String) -> WeatherRepositoryType - func create(city: String) -> WeatherRepositoryType } final class WeatherRepositoryFactory: WeatherRepositoryFactoryType { @@ -51,22 +49,4 @@ final class WeatherRepositoryFactory: WeatherRepositoryFactoryType { logger: logger ) } - - func create(zipCode: String) -> WeatherRepositoryType { - ZipCodeWeatherRepository( - appId: appId, - zipCode: zipCode, - networkClient: networkClient, - logger: logger - ) - } - - func create(city: String) -> WeatherRepositoryType { - CityWeatherRepository( - appId: appId, - city: city, - networkClient: networkClient, - logger: logger - ) - } } diff --git a/DatWeatherDoe/ViewModel/Repository/WeatherURLBuilder.swift b/DatWeatherDoe/ViewModel/Repository/WeatherURLBuilder.swift index 21f437a..25185fc 100644 --- a/DatWeatherDoe/ViewModel/Repository/WeatherURLBuilder.swift +++ b/DatWeatherDoe/ViewModel/Repository/WeatherURLBuilder.swift @@ -13,13 +13,36 @@ protocol WeatherURLBuilderType { func build() throws -> URL } -class WeatherURLBuilder: WeatherURLBuilderType { - let apiUrlString = "https://api.openweathermap.org/data/2.5/weather" - let appId: String +final class WeatherURLBuilder: WeatherURLBuilderType { + private let apiUrlString = "https://api.weatherapi.com/v1/forecast.json" + private let appId: String + private let location: CLLocationCoordinate2D - init(appId: String) { + init(appId: String, location: CLLocationCoordinate2D) { self.appId = appId + self.location = location } - func build() throws -> URL { URL(string: apiUrlString)! } + func build() throws -> URL { + let latLonString = "\(location.latitude),\(location.longitude)" + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let date = dateFormatter.string(from: Date()) + + let queryItems: [URLQueryItem] = [ + URLQueryItem(name: "key", value: appId), + URLQueryItem(name: "aqi", value: String("no")), + URLQueryItem(name: "q", value: latLonString), + URLQueryItem(name: "dt", value: date) + ] + + var urlComps = URLComponents(string: apiUrlString) + urlComps?.queryItems = queryItems + + guard let finalUrl = urlComps?.url else { + throw WeatherError.unableToConstructUrl + } + return finalUrl + } } diff --git a/DatWeatherDoe/ViewModel/Repository/Zip Code/ZipCodeValidator.swift b/DatWeatherDoe/ViewModel/Repository/Zip Code/ZipCodeValidator.swift deleted file mode 100644 index dcc627b..0000000 --- a/DatWeatherDoe/ViewModel/Repository/Zip Code/ZipCodeValidator.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ZipCodeValidator.swift -// DatWeatherDoe -// -// Created by Inder Dhir on 1/13/22. -// Copyright © 2022 Inder Dhir. All rights reserved. -// - -final class ZipCodeValidator: WeatherValidatorType { - private let zipCode: String - - init(zipCode: String) { - self.zipCode = zipCode - } - - func validate() throws { - let isZipPresent = !zipCode.isEmpty - let isZipPresentWithCountryCode = zipCode.split(separator: ",").count == 2 - let isValid = isZipPresent && isZipPresentWithCountryCode - if !isValid { - throw WeatherError.zipCodeIncorrect - } - } -} diff --git a/DatWeatherDoe/ViewModel/Repository/Zip Code/ZipCodeWeatherRepository.swift b/DatWeatherDoe/ViewModel/Repository/Zip Code/ZipCodeWeatherRepository.swift deleted file mode 100644 index 627eef2..0000000 --- a/DatWeatherDoe/ViewModel/Repository/Zip Code/ZipCodeWeatherRepository.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ZipCodeWeatherRepository.swift -// DatWeatherDoe -// -// Created by Inder Dhir on 1/14/22. -// Copyright © 2022 Inder Dhir. All rights reserved. -// - -import Foundation -import OSLog - -final class ZipCodeWeatherRepository: WeatherRepositoryType { - private let appId: String - private let zipCode: String - private let networkClient: NetworkClientType - private let logger: Logger - - init( - appId: String, - zipCode: String, - networkClient: NetworkClientType, - logger: Logger - ) { - self.appId = appId - self.zipCode = zipCode - self.networkClient = networkClient - self.logger = logger - } - - func getWeather() async throws -> WeatherAPIResponse { - logger.debug("Getting weather via zip code") - - do { - try ZipCodeValidator(zipCode: zipCode).validate() - let url = try ZipCodeWeatherURLBuilder(appId: appId, zipCode: zipCode).build() - let data = try await networkClient.performRequest(url: url) - return try WeatherAPIResponseParser().parse(data) - } catch { - logger.error("Getting weather via zip code failed") - - throw error - } - } -} diff --git a/DatWeatherDoe/ViewModel/Repository/Zip Code/ZipCodeWeatherURLBuilder.swift b/DatWeatherDoe/ViewModel/Repository/Zip Code/ZipCodeWeatherURLBuilder.swift deleted file mode 100644 index 98f0ed1..0000000 --- a/DatWeatherDoe/ViewModel/Repository/Zip Code/ZipCodeWeatherURLBuilder.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ZipCodeWeatherURLBuilder.swift -// DatWeatherDoe -// -// Created by Inder Dhir on 1/16/22. -// Copyright © 2022 Inder Dhir. All rights reserved. -// - -import Foundation - -final class ZipCodeWeatherURLBuilder: WeatherURLBuilder { - private let zipCode: String - - init(appId: String, zipCode: String) { - self.zipCode = zipCode - super.init(appId: appId) - } - - override func build() throws -> URL { - let queryItems: [URLQueryItem] = [ - URLQueryItem(name: "appid", value: appId), - URLQueryItem(name: "zip", value: zipCode) - ] - - var urlComps = URLComponents(string: apiUrlString) - urlComps?.queryItems = queryItems - guard let finalUrl = urlComps?.url else { - throw WeatherError.unableToConstructUrl - } - return finalUrl - } -} diff --git a/DatWeatherDoe/ViewModel/WeatherViewModel.swift b/DatWeatherDoe/ViewModel/WeatherViewModel.swift index 21fc87c..d9a4e08 100644 --- a/DatWeatherDoe/ViewModel/WeatherViewModel.swift +++ b/DatWeatherDoe/ViewModel/WeatherViewModel.swift @@ -37,7 +37,7 @@ final class WeatherViewModel: WeatherViewModelType { self.logger = logger weatherResult = weatherSubject.eraseToAnyPublisher() - + setupLocationFetching() } @@ -58,14 +58,10 @@ final class WeatherViewModel: WeatherViewModelType { } } - func updateCity(with cityId: Int) { - forecaster.updateCityWith(cityId: cityId) - } - func seeForecastForCurrentCity() { forecaster.seeForecastForCity() } - + private func setupLocationFetching() { locationFetcher.locationResult .sink(receiveValue: { [weak self] result in @@ -92,10 +88,6 @@ final class WeatherViewModel: WeatherViewModelType { getWeatherAfterUpdatingLocation() case .latLong: getWeatherViaLocationCoordinates() - case .zipCode: - getWeatherViaZipCode() - case .city: - getWeatherViaCity() } } @@ -103,18 +95,6 @@ final class WeatherViewModel: WeatherViewModelType { locationFetcher.startUpdatingLocation() } - private func getWeatherViaZipCode() { - guard let zipCode = configManager.weatherSourceText else { - weatherSubject.send(.failure(WeatherError.zipCodeIncorrect)) - return - } - - getWeather( - repository: weatherFactory.create(zipCode: zipCode), - unit: measurementUnit - ) - } - private func getWeatherViaLocationCoordinates() { guard let latLong = configManager.weatherSourceText else { weatherSubject.send(.failure(WeatherError.latLongIncorrect)) @@ -127,18 +107,6 @@ final class WeatherViewModel: WeatherViewModelType { ) } - private func getWeatherViaCity() { - guard let city = configManager.weatherSourceText else { - weatherSubject.send(.failure(WeatherError.cityIncorrect)) - return - } - - getWeather( - repository: weatherFactory.create(city: city), - unit: measurementUnit - ) - } - private func buildWeatherDataOptions(for unit: MeasurementUnit) -> WeatherDataBuilder.Options { .init( unit: unit, @@ -148,8 +116,8 @@ final class WeatherViewModel: WeatherViewModelType { } private func buildWeatherTextOptions(for unit: MeasurementUnit) -> WeatherTextBuilder.Options { - let conditionPosition = WeatherConditionPosition(rawValue: configManager.weatherConditionPosition) - ?? .beforeTemperature + let conditionPosition = WeatherConditionPosition(rawValue: configManager.weatherConditionPosition) + ?? .beforeTemperature return .init( isWeatherConditionAsTextEnabled: configManager.isWeatherConditionAsTextEnabled, conditionPosition: conditionPosition, diff --git a/DatWeatherDoe/ViewModel/WeatherViewModelType.swift b/DatWeatherDoe/ViewModel/WeatherViewModelType.swift index 27efeab..ef25f04 100644 --- a/DatWeatherDoe/ViewModel/WeatherViewModelType.swift +++ b/DatWeatherDoe/ViewModel/WeatherViewModelType.swift @@ -12,6 +12,5 @@ protocol WeatherViewModelType: AnyObject { var weatherResult: AnyPublisher, Never> { get } func startRefreshingWeather() - func updateCity(with cityId: Int) func seeForecastForCurrentCity() } diff --git a/DatWeatherDoeTests/API/Repository/City/CityValidatorTests.swift b/DatWeatherDoeTests/API/Repository/City/CityValidatorTests.swift deleted file mode 100644 index 8ca2ca5..0000000 --- a/DatWeatherDoeTests/API/Repository/City/CityValidatorTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// CityValidatorTests.swift -// DatWeatherDoeTests -// -// Created by preckrasno on 14.02.2023. -// Copyright © 2023 Inder Dhir. All rights reserved. -// - -@testable import DatWeatherDoe -import XCTest - -class CityValidatorTests: XCTestCase { - func testValidate_cityEmpty() { - XCTAssertThrowsError(try CityValidator(city: "").validate()) - } - - func testValidate_cityIncorrect_wrongFormat() { - XCTAssertThrowsError(try CityValidator(city: "12345").validate()) - } - - func testValidate_cityCorrect() { - XCTAssertNoThrow(try CityValidator(city: "Kyiv,ua").validate()) - } -} diff --git a/DatWeatherDoeTests/API/Repository/City/CityWeatherURLBuilderTests.swift b/DatWeatherDoeTests/API/Repository/City/CityWeatherURLBuilderTests.swift deleted file mode 100644 index e14eb94..0000000 --- a/DatWeatherDoeTests/API/Repository/City/CityWeatherURLBuilderTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// CityWeatherURLBuilderTests.swift -// DatWeatherDoeTests -// -// Created by preckrasno on 14.02.2023. -// Copyright © 2023 Inder Dhir. All rights reserved. -// - -@testable import DatWeatherDoe -import XCTest - -class CityWeatherURLBuilderTests: XCTestCase { - func testBuild() { - XCTAssertEqual( - try? CityWeatherURLBuilder(appId: "123456", city: "Kyiv,ua") - .build().absoluteString, - "https://api.openweathermap.org/data/2.5/weather?appid=123456&q=Kyiv,ua&units=metric" - ) - } -} diff --git a/DatWeatherDoeTests/API/Repository/Zip Code/ZipCodeValidatorTests.swift b/DatWeatherDoeTests/API/Repository/Zip Code/ZipCodeValidatorTests.swift deleted file mode 100644 index 4534bd8..0000000 --- a/DatWeatherDoeTests/API/Repository/Zip Code/ZipCodeValidatorTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ZipCodeValidatorTests.swift -// DatWeatherDoeTests -// -// Created by Inder Dhir on 1/26/22. -// Copyright © 2022 Inder Dhir. All rights reserved. -// - -@testable import DatWeatherDoe -import XCTest - -class ZipCodeValidatorTests: XCTestCase { - func testValidate_zipCodeEmpty() { - XCTAssertThrowsError(try ZipCodeValidator(zipCode: "").validate()) - } - - func testValidate_zipCodeIncorrect_wrongFormat() { - XCTAssertThrowsError(try ZipCodeValidator(zipCode: "12345").validate()) - } - - func testValidate_zipCodeCorrect() { - XCTAssertNoThrow(try ZipCodeValidator(zipCode: "10021,us").validate()) - } -} diff --git a/DatWeatherDoeTests/API/Repository/Zip Code/ZipCodeWeatherURLBuilderTests.swift b/DatWeatherDoeTests/API/Repository/Zip Code/ZipCodeWeatherURLBuilderTests.swift deleted file mode 100644 index 36a5047..0000000 --- a/DatWeatherDoeTests/API/Repository/Zip Code/ZipCodeWeatherURLBuilderTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ZipCodeWeatherURLBuilderTests.swift -// DatWeatherDoeTests -// -// Created by Inder Dhir on 1/25/22. -// Copyright © 2022 Inder Dhir. All rights reserved. -// - -@testable import DatWeatherDoe -import XCTest - -class ZipCodeWeatherURLBuilderTests: 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 testBuild() { - XCTAssertEqual( - try? ZipCodeWeatherURLBuilder(appId: "123456", zipCode: "10021,us") - .build().absoluteString, - "https://api.openweathermap.org/data/2.5/weather?appid=123456&zip=10021,us" - ) - } -} diff --git a/DatWeatherDoeTests/UI/Configure/Options/WeatherSourceTests.swift b/DatWeatherDoeTests/UI/Configure/Options/WeatherSourceTests.swift index 70b0b8b..acf0942 100644 --- a/DatWeatherDoeTests/UI/Configure/Options/WeatherSourceTests.swift +++ b/DatWeatherDoeTests/UI/Configure/Options/WeatherSourceTests.swift @@ -25,20 +25,4 @@ final class WeatherSourceTests: XCTestCase { XCTAssertEqual(latLongSource.placeholder, "42,42") XCTAssertEqual(latLongSource.textHint, "[latitude],[longitude]") } - - func testZipCodeSource() { - let zipCodeSource = WeatherSource.zipCode - - XCTAssertEqual(zipCodeSource.title, "Zip Code") - XCTAssertEqual(zipCodeSource.placeholder, "10021,us") - XCTAssertEqual(zipCodeSource.textHint, "[zipcode],[iso 3166 country code]") - } - - func testCitySource() { - let citySource = WeatherSource.city - - XCTAssertEqual(citySource.title, "City") - XCTAssertEqual(citySource.placeholder, "Kyiv,ua") - XCTAssertEqual(citySource.textHint, "[city],[iso 3166 country code]") - } } diff --git a/README.md b/README.md index 706c3c6..f24d4cf 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # [](image.png) DatWeatherDoe -- Fetch weather using: - - Location services - - Zip Code - - Latitude / Longitude - - City +> **Note** +OpenWeatherMap API 2.5 support is ending in June 2024. The app uses WeatherAPI going forward with location support. + +- Fetch weather using: + - Location services + - Zip Code + - Latitude / Longitude + - City - Configurable polling interval - Dark mode support - Supports MacOS 13.0+ @@ -23,7 +26,7 @@ ### Manual -https://github.com/inderdhir/DatWeatherDoe/releases/latest + ## Using Location Services @@ -36,10 +39,11 @@ If using location, please make sure that the app has permission to access locati ## Developer Setup -- Get your personal API key for openweathermap [here](http://openweathermap.org/appid) +- Get your personal API key for WeatherAPI [here](https://www.weatherapi.com) - Add the following in "Config.xcconfig": -``` -OPENWEATHERMAP_APP_ID = YOUR_KEY + +```env +WEATHER_API_KEY=YOUR_KEY ``` ## Donate