From 7a81c06c739402d831665ccaef39c6c37269cf4b Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Tue, 15 Dec 2020 23:39:28 +0100 Subject: [PATCH] Version 4.0 (#11) - Renamed HPOpenWeather to OpenWeather - Updated some method signatures - greatly simplified decoding of responses which improves readability - added more documentation comments - added alerts --- .github/workflows/swift.yml | 3 + HPOpenWeather.podspec | 2 +- Package.swift | 11 +- README.md | 30 ++- Sources/HPOpenWeather/DataTypes/Alert.swift | 25 +++ .../DataTypes/CurrentWeather.swift | 94 --------- .../DataTypes/DailyForecast.swift | 97 --------- .../DataTypes/DailyTemperature.swift | 7 + .../{ => Forecasts}/BasicWeather.swift | 0 .../DataTypes/Forecasts/CurrentWeather.swift | 75 +++++++ .../DataTypes/Forecasts/DailyForecast.swift | 70 +++++++ .../DataTypes/Forecasts/HourlyForecast.swift | 64 ++++++ .../DataTypes/HourlyForecast.swift | 86 -------- .../DataTypes/Precipitation.swift | 3 - .../HPOpenWeather/DataTypes/Temperature.swift | 3 + .../DataTypes/WeatherCondition.swift | 15 +- .../HPOpenWeather/DataTypes/WeatherIcon.swift | 188 +++++++++--------- Sources/HPOpenWeather/DecodableDefault.swift | 91 +++++++++ .../CLLocationCoordinate+Extensions.swift} | 0 .../{ => Extensions}/NSError+Extensions.swift | 3 + .../URLQueryItemsBuilder+Extensions.swift | 0 ...{HPOpenWeather.swift => OpenWeather.swift} | 29 ++- .../Requests/APINetworkRequest.swift | 18 ++ .../Requests/OpenWeatherRequest+Combine.swift | 49 +++++ .../Requests/OpenWeatherRequest.swift | 60 +----- .../Requests/RequestLanguage.swift | 1 + .../HPOpenWeather/Requests/RequestUnits.swift | 5 + .../Requests/TimeMachineRequest.swift | 37 +--- .../Requests/WeatherRequest.swift | 46 ++--- .../Response/TimeMachineResponse.swift | 37 +--- .../Response/WeatherResponse.swift | 42 ++-- .../HPOpenWeatherTests.swift | 15 +- .../HPOpenWeatherTests/WeatherIconTests.swift | 15 ++ 33 files changed, 617 insertions(+), 604 deletions(-) create mode 100644 Sources/HPOpenWeather/DataTypes/Alert.swift delete mode 100644 Sources/HPOpenWeather/DataTypes/CurrentWeather.swift delete mode 100644 Sources/HPOpenWeather/DataTypes/DailyForecast.swift rename Sources/HPOpenWeather/DataTypes/{ => Forecasts}/BasicWeather.swift (100%) create mode 100644 Sources/HPOpenWeather/DataTypes/Forecasts/CurrentWeather.swift create mode 100644 Sources/HPOpenWeather/DataTypes/Forecasts/DailyForecast.swift create mode 100644 Sources/HPOpenWeather/DataTypes/Forecasts/HourlyForecast.swift delete mode 100644 Sources/HPOpenWeather/DataTypes/HourlyForecast.swift create mode 100644 Sources/HPOpenWeather/DecodableDefault.swift rename Sources/HPOpenWeather/{DataTypes/CLLocationCoordinate+Codable.swift => Extensions/CLLocationCoordinate+Extensions.swift} (100%) rename Sources/HPOpenWeather/{ => Extensions}/NSError+Extensions.swift (59%) rename Sources/HPOpenWeather/{Requests => Extensions}/URLQueryItemsBuilder+Extensions.swift (100%) rename Sources/HPOpenWeather/{HPOpenWeather.swift => OpenWeather.swift} (68%) create mode 100644 Sources/HPOpenWeather/Requests/APINetworkRequest.swift create mode 100644 Sources/HPOpenWeather/Requests/OpenWeatherRequest+Combine.swift create mode 100644 Tests/HPOpenWeatherTests/WeatherIconTests.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index dc93a94..a8f4bf0 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -12,6 +12,9 @@ jobs: runs-on: macos-latest steps: + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable - uses: actions/checkout@v2 - name: Build run: swift build -v diff --git a/HPOpenWeather.podspec b/HPOpenWeather.podspec index 659540c..a5913e6 100644 --- a/HPOpenWeather.podspec +++ b/HPOpenWeather.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "HPOpenWeather" - s.version = "3.6.0" + s.version = "4.0.0" s.summary = "Cross-platform framework to communicate with the OpenWeatherMap JSON API" s.license = { :type => "MIT", :file => "LICENSE.md" } diff --git a/Package.swift b/Package.swift index 767a5fc..8bc2035 100644 --- a/Package.swift +++ b/Package.swift @@ -12,20 +12,23 @@ let package = Package( // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "HPOpenWeather", - targets: ["HPOpenWeather"]) + targets: ["HPOpenWeather"] + ) ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/henrik-dmg/HPNetwork", from: "0.1.0") + .package(url: "https://github.com/henrik-dmg/HPNetwork", from: "0.8.0") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "HPOpenWeather", - dependencies: ["HPNetwork"]), + dependencies: ["HPNetwork"] + ), .testTarget( name: "HPOpenWeatherTests", - dependencies: ["HPOpenWeather"]) + dependencies: ["HPOpenWeather"] + ) ] ) diff --git a/README.md b/README.md index fa0c42d..aba24ff 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,14 @@ Github Actions [![GitHub license](https://img.shields.io/github/license/henrik-dmg/HPOpenWeather)](https://github.com/henrik-dmg/HPOpenWeather/blob/master/LICENSE.md) -HPOpenWeather is a cross-platform Swift framework to communicate with the OpenWeatherMap JSON API. See their [documentation](https://openweathermap.org/api) for further details. +HPOpenWeather is a cross-platform Swift framework to communicate with the OpenWeather One-Call API. See their [documentation](https://openweathermap.org/api/one-call-api) for further details. ## Installation -HPOpenWeather supports iOS 9.0+, watchOS 2.0+, tvOS 9.0+ and macOS 10.10+. +HPOpenWeather supports iOS 9.0+, watchOS 3.0+, tvOS 9.0+ and macOS 10.10+. #### SPM -Add `.package(url: "https://github.com/henrik-dmg/HPOpenWeather", from: "3.0.0")` to your `Package.swift` file +Add `.package(url: "https://github.com/henrik-dmg/HPOpenWeather", from: "4.0.0")` to your `Package.swift` file #### CocoaPods @@ -22,12 +22,18 @@ Add `pod 'HPOpenWeather'` to your `Podfile` and run `pod install` ## Usage -To get started, you need an API key from [OpenWeatherMap](https://openweathermap.org). Put this API key in the initialiser, additionally you can also specify a custom temperature format and/or language used in the responses (see list for available languages and units below). +To get started, you need an API key from [OpenWeather](https://openweathermap.org). Put this API key in the initialiser, additionally you can also specify a custom temperature format and/or language used in the responses (see list for available languages and units below). ```swift import HPOpenWeather // Assign API key -HPOpenWeather.shared.apiKey = "--- YOUR API KEY ---" +OpenWeather.shared.apiKey = "--- YOUR API KEY ---" +OpenWeather.shared.language = .german +OpenWeather.shared.units = .metric + +// Or use options +let settings = OpenWeather.Settings(apiKey: "yourAPIKey", language: .german, units: .metric) +OpenWeather.shared.apply(settings) ``` You can also customise the response data units and language by accessing the `language` and `units` propertis. @@ -47,10 +53,10 @@ let timemachineRequest = TimeMachineRequest(coordinate: .init(latitude: 40, long **Note:** the date has to be at least 6 hours in the past -To post a request, call the `requestWeather` on `HPOpenWeather`: +To post a request, call `sendWeatherRequest` on `OpenWeather`: ```swift -HPOpenWeather.shared.requestWeather(request) { result in +OpenWeather.shared.sendWeatherRequest(request) { result in switch result { case .success(let response): // do something with weather data here @@ -87,13 +93,3 @@ HPOpenWeather.shared.requestWeather(request) { result in - Celsius (default) - Kelvin - Fahrenheit - -## TODO List -- [x] Current weather data -- [x] Daily and hourly forecast -- [x] More Unit Tests -- [x] Historical Data -- [ ] UV Index Data -- [ ] watchOS and tvOS demo apps - -#### This documentation is far from complete, however the code itself is pretty well documented so feel free to just play around and just contact me if you have any suggestions :) diff --git a/Sources/HPOpenWeather/DataTypes/Alert.swift b/Sources/HPOpenWeather/DataTypes/Alert.swift new file mode 100644 index 0000000..426a519 --- /dev/null +++ b/Sources/HPOpenWeather/DataTypes/Alert.swift @@ -0,0 +1,25 @@ +import Foundation + +/// Type that holds information about weather alerts +public struct Alert: Codable, Hashable, Equatable { + + /// Name of the alert source. Please read here the full list of alert sources + public let senderName: String + /// Alert event name + public let eventName: String + //// Date and time of the start of the alert + public let startDate: Date + //// Date and time of the end of the alert + public let endDate: Date + /// Description of the alert + public let description: String + + enum CodingKeys: String, CodingKey { + case senderName = "sender_name" + case eventName = "event" + case startDate = "start" + case endDate = "end" + case description + } + +} diff --git a/Sources/HPOpenWeather/DataTypes/CurrentWeather.swift b/Sources/HPOpenWeather/DataTypes/CurrentWeather.swift deleted file mode 100644 index 965b5da..0000000 --- a/Sources/HPOpenWeather/DataTypes/CurrentWeather.swift +++ /dev/null @@ -1,94 +0,0 @@ -import Foundation - -public struct CurrentWeather: BasicWeatherResponse, SunResponse { - - public let timestamp: Date - public let pressure: Double? - public let humidity: Double? - public let dewPoint: Double? - public let uvIndex: Double? - public let visibility: Double? - public let cloudCoverage: Double? - public let wind: Wind - public let temperature: Temperature - public let rain: Precipitation - public let snow: Precipitation - public let sun: Sun - private let weatherArray: [WeatherCondition] - - public var condition: WeatherCondition { - weatherArray.first ?? WeatherCondition.unknown - } - - enum CodingKeys: String, CodingKey { - case feelsLike = "feels_like" - case snow - case rain - case timestamp = "dt" - case temperature = "temp" - case pressure - case humidity - case dewPoint = "dew_point" - case uvIndex = "uvi" - case cloudCoverage = "clouds" - case visibility - case windSpeed = "wind_speed" - case windGust = "wind_gust" - case windDirection = "wind_deg" - case weatherArray = "weather" - case sunrise - case sunset - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - snow = try container.decodeIfPresent(Precipitation.self, forKey: .snow) ?? Precipitation.none - rain = try container.decodeIfPresent(Precipitation.self, forKey: .rain) ?? Precipitation.none - timestamp = try container.decode(Date.self, forKey: .timestamp) - weatherArray = try container.decode([WeatherCondition].self, forKey: .weatherArray) - pressure = try container.decodeIfPresent(Double.self, forKey: .pressure) - humidity = try container.decodeIfPresent(Double.self, forKey: .humidity) - dewPoint = try container.decodeIfPresent(Double.self, forKey: .dewPoint) - uvIndex = try container.decodeIfPresent(Double.self, forKey: .uvIndex) - visibility = try container.decodeIfPresent(Double.self, forKey: .visibility) - cloudCoverage = try container.decodeIfPresent(Double.self, forKey: .cloudCoverage) - - let temp = try container.decode(Double.self, forKey: .temperature) - let feelsLike = try container.decode(Double.self, forKey: .feelsLike) - temperature = Temperature(actual: temp, feelsLike: feelsLike) - - let windSpeed = try container.decodeIfPresent(Double.self, forKey: .windSpeed) - let windGust = try container.decodeIfPresent(Double.self, forKey: .windGust) - let windDirection = try container.decodeIfPresent(Double.self, forKey: .windDirection) - wind = Wind(speed: windSpeed, gust: windGust, degrees: windDirection) - - let sunrise = try container.decode(Date.self, forKey: .sunrise) - let sunset = try container.decode(Date.self, forKey: .sunset) - sun = Sun(sunset: sunset, sunrise: sunrise) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(temperature.actual, forKey: .temperature) - try container.encode(temperature.feelsLike, forKey: .feelsLike) - try container.encode(timestamp, forKey: .timestamp) - try container.encode(pressure, forKey: .pressure) - try container.encode(humidity, forKey: .humidity) - try container.encode(dewPoint, forKey: .dewPoint) - try container.encode(uvIndex, forKey: .uvIndex) - try container.encode(visibility, forKey: .visibility) - try container.encode(cloudCoverage, forKey: .cloudCoverage) - try container.encode(rain, forKey: .rain) - try container.encode(snow, forKey: .snow) - - try container.encode(wind.speed, forKey: .windSpeed) - try container.encode(wind.degrees, forKey: .windDirection) - try container.encode(wind.gust, forKey: .windGust) - try container.encode(sun.sunset, forKey: .sunset) - try container.encode(sun.sunrise, forKey: .sunrise) - try container.encode(weatherArray, forKey: .weatherArray) - } - -} diff --git a/Sources/HPOpenWeather/DataTypes/DailyForecast.swift b/Sources/HPOpenWeather/DataTypes/DailyForecast.swift deleted file mode 100644 index c941da8..0000000 --- a/Sources/HPOpenWeather/DataTypes/DailyForecast.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation - -public struct DailyForecast: BasicWeatherResponse, SunResponse { - - public let temperature: DailyTemperature - public let feelsLikeTemperature: DailyTemperature - public let totalRain: Double? - public let totalSnow: Double? - - public let timestamp: Date - public let pressure: Double? - public let humidity: Double? - public let dewPoint: Double? - public let uvIndex: Double? - public let visibility: Double? - public let cloudCoverage: Double? - public let wind: Wind - public let sun: Sun - private let weatherArray: [WeatherCondition] - - public var condition: WeatherCondition { - weatherArray.first ?? WeatherCondition.unknown - } - - enum CodingKeys: String, CodingKey { - case feelsLike = "feels_like" - case snow - case rain - case timestamp = "dt" - case temperature = "temp" - case pressure - case humidity - case dewPoint = "dew_point" - case uvIndex = "uvi" - case cloudCoverage = "clouds" - case visibility - case windSpeed = "wind_speed" - case windGust = "wind_gust" - case windDirection = "wind_deg" - case weatherArray = "weather" - case sunrise - case sunset - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - temperature = try container.decode(DailyTemperature.self, forKey: .temperature) - feelsLikeTemperature = try container.decode(DailyTemperature.self, forKey: .feelsLike) - totalRain = try container.decodeIfPresent(Double.self, forKey: .rain) - totalSnow = try container.decodeIfPresent(Double.self, forKey: .snow) - - timestamp = try container.decode(Date.self, forKey: .timestamp) - weatherArray = try container.decode([WeatherCondition].self, forKey: .weatherArray) - pressure = try container.decodeIfPresent(Double.self, forKey: .pressure) - humidity = try container.decodeIfPresent(Double.self, forKey: .humidity) - dewPoint = try container.decodeIfPresent(Double.self, forKey: .dewPoint) - uvIndex = try container.decodeIfPresent(Double.self, forKey: .uvIndex) - visibility = try container.decodeIfPresent(Double.self, forKey: .visibility) - cloudCoverage = try container.decodeIfPresent(Double.self, forKey: .cloudCoverage) - - let windSpeed = try container.decodeIfPresent(Double.self, forKey: .windSpeed) - let windGust = try container.decodeIfPresent(Double.self, forKey: .windGust) - let windDirection = try container.decodeIfPresent(Double.self, forKey: .windDirection) - wind = Wind(speed: windSpeed, gust: windGust, degrees: windDirection) - - let sunrise = try container.decode(Date.self, forKey: .sunrise) - let sunset = try container.decode(Date.self, forKey: .sunset) - sun = Sun(sunset: sunset, sunrise: sunrise) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(temperature, forKey: .temperature) - try container.encode(feelsLikeTemperature, forKey: .feelsLike) - try container.encode(totalRain, forKey: .rain) - try container.encode(totalSnow, forKey: .snow) - - try container.encode(timestamp, forKey: .timestamp) - try container.encode(pressure, forKey: .pressure) - try container.encode(humidity, forKey: .humidity) - try container.encode(dewPoint, forKey: .dewPoint) - try container.encode(uvIndex, forKey: .uvIndex) - try container.encode(visibility, forKey: .visibility) - try container.encode(cloudCoverage, forKey: .cloudCoverage) - - try container.encode(wind.speed, forKey: .windSpeed) - try container.encode(wind.degrees, forKey: .windDirection) - try container.encode(wind.gust, forKey: .windGust) - try container.encode(sun.sunset, forKey: .sunset) - try container.encode(sun.sunrise, forKey: .sunrise) - try container.encode(weatherArray, forKey: .weatherArray) - } - -} - diff --git a/Sources/HPOpenWeather/DataTypes/DailyTemperature.swift b/Sources/HPOpenWeather/DataTypes/DailyTemperature.swift index 45f42d6..d323bd6 100644 --- a/Sources/HPOpenWeather/DataTypes/DailyTemperature.swift +++ b/Sources/HPOpenWeather/DataTypes/DailyTemperature.swift @@ -1,12 +1,19 @@ import Foundation +/// Type that holds information about daily temperature changes public struct DailyTemperature: Codable, Equatable, Hashable { + /// Day temperature. public let day: Double + /// Night temperature. public let night: Double + /// Minimum daily temperature. public let min: Double? + /// Max daily temperature. public let max: Double? + /// Evening temperature. public let evening: Double + /// Morning temperature. public let morning: Double enum CodingKeys: String, CodingKey { diff --git a/Sources/HPOpenWeather/DataTypes/BasicWeather.swift b/Sources/HPOpenWeather/DataTypes/Forecasts/BasicWeather.swift similarity index 100% rename from Sources/HPOpenWeather/DataTypes/BasicWeather.swift rename to Sources/HPOpenWeather/DataTypes/Forecasts/BasicWeather.swift diff --git a/Sources/HPOpenWeather/DataTypes/Forecasts/CurrentWeather.swift b/Sources/HPOpenWeather/DataTypes/Forecasts/CurrentWeather.swift new file mode 100644 index 0000000..34694ae --- /dev/null +++ b/Sources/HPOpenWeather/DataTypes/Forecasts/CurrentWeather.swift @@ -0,0 +1,75 @@ +import Foundation + +public struct CurrentWeather: BasicWeatherResponse, SunResponse { + + // MARK: - Coding Keys + + enum CodingKeys: String, CodingKey { + case feelsLikeTemperature = "feels_like" + case snow + case rain + case timestamp = "dt" + case actualTemperature = "temp" + case pressure + case humidity + case dewPoint = "dew_point" + case uvIndex = "uvi" + case cloudCoverage = "clouds" + case visibility + case windSpeed = "wind_speed" + case windGust = "wind_gust" + case windDirection = "wind_deg" + case weatherArray = "weather" + case sunrise + case sunset + } + + // MARK: - Properties + + public let timestamp: Date + public let pressure: Double? + public let humidity: Double? + public let dewPoint: Double? + public let uvIndex: Double? + public let visibility: Double? + public let cloudCoverage: Double? + public let rain: Precipitation? + public let snow: Precipitation? + + // Temperature + + private let actualTemperature: Double + private let feelsLikeTemperature: Double + + public var temperature: Temperature { + Temperature(actual: actualTemperature, feelsLike: feelsLikeTemperature) + } + + // Weather Conditions + + private let weatherArray: [WeatherCondition] + + public var condition: WeatherCondition? { + weatherArray.first + } + + // Wind + + private let windSpeed: Double? + private let windGust: Double? + private let windDirection: Double? + + public var wind: Wind { + Wind(speed: windSpeed, gust: windGust, degrees: windDirection) + } + + // Sun + + private let sunrise: Date + private let sunset: Date + + public var sun: Sun { + Sun(sunset: sunset, sunrise: sunrise) + } + +} diff --git a/Sources/HPOpenWeather/DataTypes/Forecasts/DailyForecast.swift b/Sources/HPOpenWeather/DataTypes/Forecasts/DailyForecast.swift new file mode 100644 index 0000000..f466dab --- /dev/null +++ b/Sources/HPOpenWeather/DataTypes/Forecasts/DailyForecast.swift @@ -0,0 +1,70 @@ +import Foundation + +public struct DailyForecast: BasicWeatherResponse, SunResponse { + + // MARK: - Coding Keys + + enum CodingKeys: String, CodingKey { + case feelsLikeTemperature = "feels_like" + case totalRain = "rain" + case totalSnow = "snow" + case timestamp = "dt" + case temperature = "temp" + case pressure + case humidity + case dewPoint = "dew_point" + case uvIndex = "uvi" + case cloudCoverage = "clouds" + case visibility + case windSpeed = "wind_speed" + case windGust = "wind_gust" + case windDirection = "wind_deg" + case weatherArray = "weather" + case sunrise + case sunset + } + + // MARK: - Properties + + public let temperature: DailyTemperature + public let feelsLikeTemperature: DailyTemperature + public let totalRain: Double? + public let totalSnow: Double? + + public let timestamp: Date + public let pressure: Double? + public let humidity: Double? + public let dewPoint: Double? + public let uvIndex: Double? + public let visibility: Double? + public let cloudCoverage: Double? + + // Weather Conditions + + private let weatherArray: [WeatherCondition] + + public var condition: WeatherCondition? { + weatherArray.first + } + + // Wind + + private let windSpeed: Double? + private let windGust: Double? + private let windDirection: Double? + + public var wind: Wind { + Wind(speed: windSpeed, gust: windGust, degrees: windDirection) + } + + // Sun + + private let sunrise: Date + private let sunset: Date + + public var sun: Sun { + Sun(sunset: sunset, sunrise: sunrise) + } + +} + diff --git a/Sources/HPOpenWeather/DataTypes/Forecasts/HourlyForecast.swift b/Sources/HPOpenWeather/DataTypes/Forecasts/HourlyForecast.swift new file mode 100644 index 0000000..62febc2 --- /dev/null +++ b/Sources/HPOpenWeather/DataTypes/Forecasts/HourlyForecast.swift @@ -0,0 +1,64 @@ +import Foundation + +public struct HourlyForecast: BasicWeatherResponse { + + // MARK: - Coding Keys + + enum CodingKeys: String, CodingKey { + case actualTemperature = "temp" + case feelsLikeTemperature = "feels_like" + case snow + case rain + case timestamp = "dt" + case pressure + case humidity + case dewPoint = "dew_point" + case uvIndex = "uvi" + case cloudCoverage = "clouds" + case visibility + case windSpeed = "wind_speed" + case windGust = "wind_gust" + case windDirection = "wind_deg" + case weatherArray = "weather" + } + + // MARK: - Properties + + public let timestamp: Date + public let pressure: Double? + public let humidity: Double? + public let dewPoint: Double? + public let uvIndex: Double? + public let visibility: Double? + public let cloudCoverage: Double? + public let rain: Precipitation? + public let snow: Precipitation? + + // Temperature + + private let actualTemperature: Double + private let feelsLikeTemperature: Double + + public var temperature: Temperature { + Temperature(actual: actualTemperature, feelsLike: feelsLikeTemperature) + } + + // Wind + + private let windSpeed: Double? + private let windGust: Double? + private let windDirection: Double? + + public var wind: Wind { + Wind(speed: windSpeed, gust: windGust, degrees: windDirection) + } + + // Weather + + private let weatherArray: [WeatherCondition] + + public var weather: [WeatherCondition] { + weatherArray + } + +} diff --git a/Sources/HPOpenWeather/DataTypes/HourlyForecast.swift b/Sources/HPOpenWeather/DataTypes/HourlyForecast.swift deleted file mode 100644 index 5ca4418..0000000 --- a/Sources/HPOpenWeather/DataTypes/HourlyForecast.swift +++ /dev/null @@ -1,86 +0,0 @@ -import Foundation - -public struct HourlyForecast: BasicWeatherResponse { - - public let timestamp: Date - public let pressure: Double? - public let humidity: Double? - public let dewPoint: Double? - public let uvIndex: Double? - public let visibility: Double? - public let cloudCoverage: Double? - public let wind: Wind - public let temperature: Temperature - public let rain: Precipitation - public let snow: Precipitation - - let weatherArray: [WeatherCondition] - - public var weather: [WeatherCondition] { - weatherArray - } - - enum CodingKeys: String, CodingKey { - case temperature = "temp" - case feelsLike = "feels_like" - case snow - case rain - case timestamp = "dt" - case pressure - case humidity - case dewPoint = "dew_point" - case uvIndex = "uvi" - case cloudCoverage = "clouds" - case visibility - case windSpeed = "wind_speed" - case windGust = "wind_gust" - case windDirection = "wind_deg" - case weatherArray = "weather" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - timestamp = try container.decode(Date.self, forKey: .timestamp) - snow = try container.decodeIfPresent(Precipitation.self, forKey: .snow) ?? Precipitation.none - rain = try container.decodeIfPresent(Precipitation.self, forKey: .rain) ?? Precipitation.none - weatherArray = try container.decode([WeatherCondition].self, forKey: .weatherArray) - pressure = try container.decodeIfPresent(Double.self, forKey: .pressure) - humidity = try container.decodeIfPresent(Double.self, forKey: .humidity) - dewPoint = try container.decodeIfPresent(Double.self, forKey: .dewPoint) - uvIndex = try container.decodeIfPresent(Double.self, forKey: .uvIndex) - visibility = try container.decodeIfPresent(Double.self, forKey: .visibility) - cloudCoverage = try container.decodeIfPresent(Double.self, forKey: .cloudCoverage) - - let temp = try container.decode(Double.self, forKey: .temperature) - let feelsLike = try container.decode(Double.self, forKey: .feelsLike) - temperature = Temperature(actual: temp, feelsLike: feelsLike) - - let windSpeed = try container.decodeIfPresent(Double.self, forKey: .windSpeed) - let windGust = try container.decodeIfPresent(Double.self, forKey: .windGust) - let windDirection = try container.decodeIfPresent(Double.self, forKey: .windDirection) - wind = Wind(speed: windSpeed, gust: windGust, degrees: windDirection) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(temperature.actual, forKey: .temperature) - try container.encode(temperature.feelsLike, forKey: .feelsLike) - try container.encode(timestamp, forKey: .timestamp) - try container.encode(pressure, forKey: .pressure) - try container.encode(humidity, forKey: .humidity) - try container.encode(dewPoint, forKey: .dewPoint) - try container.encode(uvIndex, forKey: .uvIndex) - try container.encode(visibility, forKey: .visibility) - try container.encode(cloudCoverage, forKey: .cloudCoverage) - try container.encode(rain, forKey: .rain) - try container.encode(snow, forKey: .snow) - - try container.encode(wind.speed, forKey: .windSpeed) - try container.encode(wind.degrees, forKey: .windDirection) - try container.encode(wind.gust, forKey: .windGust) - try container.encode(weatherArray, forKey: .weatherArray) - } - -} diff --git a/Sources/HPOpenWeather/DataTypes/Precipitation.swift b/Sources/HPOpenWeather/DataTypes/Precipitation.swift index 6c388fd..d40ee0f 100644 --- a/Sources/HPOpenWeather/DataTypes/Precipitation.swift +++ b/Sources/HPOpenWeather/DataTypes/Precipitation.swift @@ -8,9 +8,6 @@ public struct Precipitation: Codable, Equatable, Hashable { /// Precipitation volume for the last 3 hours, measured in mm public var lastThreeHours: Double? - /// Singleton property to indicate there was no precipitation within the last 3 hours - static let none = Precipitation(lastHour: nil, lastThreeHours: nil) - enum CodingKeys: String, CodingKey { case lastHour = "1h" case lastThreeHours = "3h" diff --git a/Sources/HPOpenWeather/DataTypes/Temperature.swift b/Sources/HPOpenWeather/DataTypes/Temperature.swift index a9635cd..63686bd 100644 --- a/Sources/HPOpenWeather/DataTypes/Temperature.swift +++ b/Sources/HPOpenWeather/DataTypes/Temperature.swift @@ -1,8 +1,11 @@ import Foundation +/// Type that holds information about daily temperature changes public struct Temperature: Codable, Equatable, Hashable { + /// The actually measured temperature public let actual: Double + /// The feels-like temperature public let feelsLike: Double } diff --git a/Sources/HPOpenWeather/DataTypes/WeatherCondition.swift b/Sources/HPOpenWeather/DataTypes/WeatherCondition.swift index 790b41d..8478364 100644 --- a/Sources/HPOpenWeather/DataTypes/WeatherCondition.swift +++ b/Sources/HPOpenWeather/DataTypes/WeatherCondition.swift @@ -10,19 +10,6 @@ public struct WeatherCondition: Codable, Equatable, Hashable { /// The weather condition within the group public let description: String /// The ID of the corresponding weather icon - public let iconString: String - - static let unknown = WeatherCondition( - id: 0, - main: "Unknown Weather Condition", - description: "No Description", - iconString: "No Icon") - - enum CodingKeys: String, CodingKey { - case id - case main - case description - case iconString = "icon" - } + public let icon: WeatherIcon } diff --git a/Sources/HPOpenWeather/DataTypes/WeatherIcon.swift b/Sources/HPOpenWeather/DataTypes/WeatherIcon.swift index 6a21555..af80447 100644 --- a/Sources/HPOpenWeather/DataTypes/WeatherIcon.swift +++ b/Sources/HPOpenWeather/DataTypes/WeatherIcon.swift @@ -1,105 +1,97 @@ #if canImport(UIKit) -import SwiftUI import UIKit +#elseif canImport(AppKit) +import AppKit +#endif +import SwiftUI +import Foundation + +public enum WeatherIcon: String, Codable, CaseIterable { + + case clearSky = "01d" + case clearSkyNight = "01n" + case fewClouds = "02d" + case fewCloudsNight = "02n" + case scatteredClouds = "03d" + case scatteredCloudsNight = "03n" + case brokenClouds = "04d" + case brokenCloudsNight = "04n" + case showerRain = "09d" + case showerRainNight = "09n" + case rain = "10d" + case rainNight = "10n" + case thunderstorm = "11d" + case thunderstormNight = "11n" + case snow = "13d" + case snowNight = "13n" + case mist = "50d" + case mistNight = "50n" -extension WeatherCondition { - - /// The corresponding system weather icon - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) - public var systemIcon: WeatherSystemIcon? { - return WeatherIcon.make(from: iconString) - } - -} - -public enum WeatherIcon: String { - - case clearSky = "01" - case fewClouds = "02" - case scatteredClouds = "03" - case brokenClouds = "04" - case showerRain = "09" - case rain = "10" - case thunderstorm = "11" - case snow = "13" - case mist = "50" - - init?(apiCode: String) { - let code = String(apiCode.dropLast()) - self.init(rawValue: code) - } - - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) - var day: WeatherSystemIcon { - return WeatherSystemIcon(icon: self, night: false) - } - - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) - var night: WeatherSystemIcon { - return WeatherSystemIcon(icon: self, night: true) - } - - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) - static func make(from iconName: String) -> WeatherSystemIcon? { - guard iconName.count == 3, let iconType = WeatherIcon(apiCode: iconName) else { - return nil - } - - let isNightIcon = iconName.last! == "n" - return isNightIcon ? iconType.night : iconType.day - } - } -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) -public struct WeatherSystemIcon { - - private let icon: WeatherIcon - private let isNightIcon: Bool - - fileprivate init(icon: WeatherIcon, night: Bool) { - self.icon = icon - self.isNightIcon = night - } - - public var regularUIIcon: UIImage { - UIImage(systemName: iconName(filled: false))! - } - - public var filledUIIcon: UIImage { - UIImage(systemName: iconName(filled: true))! - } - - public func iconName(filled: Bool) -> String { - var iconName = "" - switch self.icon { - case .clearSky: - iconName = isNightIcon ? "moon" : "sun.max" - case .fewClouds: - iconName = isNightIcon ? "cloud.moon" : "cloud.sun" - case .scatteredClouds: - iconName = "cloud" - case .brokenClouds: - iconName = "smoke" - case .showerRain: - iconName = "cloud.rain" - case .rain: - iconName = isNightIcon ? "cloud.moon.rain" : "cloud.sun.rain" - case .thunderstorm: - iconName = "cloud.bolt.rain" - case .snow: - return "snow" - case .mist: - iconName = "cloud.fog" - } - - if filled { - iconName.append(".fill") - } - - return iconName - } +@available(OSX 11.0, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension WeatherIcon { + + #if canImport(UIKit) + + public func filledUIImage(withConfiguration configuration: UIImage.Configuration? = nil) -> UIImage? { + UIImage(systemName: makeIconName(filled: true), withConfiguration: configuration) + } + + public func outlineUIImage(withConfiguration configuration: UIImage.Configuration? = nil) -> UIImage? { + UIImage(systemName: makeIconName(filled: false), withConfiguration: configuration) + } + + #elseif canImport(AppKit) + + public func filledNSImage(accessibilityDescription: String? = nil) -> NSImage? { + NSImage(systemSymbolName: makeIconName(filled: true), accessibilityDescription: accessibilityDescription) + } + + public func outlineNSImage(accessibilityDescription: String? = nil) -> NSImage? { + NSImage(systemSymbolName: makeIconName(filled: false), accessibilityDescription: accessibilityDescription) + } + + #endif + + public func filledImage() -> Image { + Image(systemName: makeIconName(filled: true)) + } + + public func outlineImage() -> Image { + Image(systemName: makeIconName(filled: false)) + } + + private func makeIconName(filled: Bool) -> String { + let iconName: String + switch self { + case .clearSky: + iconName = "sun.max" + case .clearSkyNight: + iconName = "moon" + case .fewClouds: + iconName = "cloud.sun" + case .fewCloudsNight: + iconName = "cloud.moon" + case .scatteredClouds, .scatteredCloudsNight: + iconName = "cloud" + case .brokenClouds, .brokenCloudsNight: + iconName = "smoke" + case .showerRain, .showerRainNight: + iconName = "cloud.rain" + case .rain: + iconName = "cloud.sun.rain" + case .rainNight: + iconName = "cloud.moon.rain" + case .thunderstorm, .thunderstormNight: + iconName = "cloud.bolt.rain" + case .snow, .snowNight: + return "snow" + case .mist, .mistNight: + iconName = "cloud.fog" + } + + return iconName + (filled ? ".fill" : "") + } } - -#endif diff --git a/Sources/HPOpenWeather/DecodableDefault.swift b/Sources/HPOpenWeather/DecodableDefault.swift new file mode 100644 index 0000000..e9e4858 --- /dev/null +++ b/Sources/HPOpenWeather/DecodableDefault.swift @@ -0,0 +1,91 @@ +import Foundation + +public protocol DecodableDefaultSource { + associatedtype Value: Decodable + static var defaultValue: Value { get } +} + +public enum DecodableDefault {} + +public extension DecodableDefault { + + @propertyWrapper struct Wrapper { + + public typealias Value = Source.Value + public var wrappedValue = Source.defaultValue + + public init(wrappedValue: Source.Value = Source.defaultValue) { + self.wrappedValue = wrappedValue + } + + } + +} + +extension DecodableDefault.Wrapper: Decodable { + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + wrappedValue = try container.decode(Value.self) + } + +} + +public extension KeyedDecodingContainer { + + func decode(_ type: DecodableDefault.Wrapper.Type, forKey key: Key) throws -> DecodableDefault.Wrapper { + try decodeIfPresent(type, forKey: key) ?? .init() + } + +} + +public extension DecodableDefault { + + typealias Source = DecodableDefaultSource + typealias List = Decodable & ExpressibleByArrayLiteral + typealias Map = Decodable & ExpressibleByDictionaryLiteral + + enum Sources { + public enum True: Source { + public static var defaultValue: Bool { true } + } + + public enum False: Source { + public static var defaultValue: Bool { false } + } + + public enum EmptyString: Source { + public static var defaultValue: String { "" } + } + + public enum EmptyList: Source { + public static var defaultValue: T { [] } + } + + public enum EmptyMap: Source { + public static var defaultValue: T { [:] } + } + } + +} + +public extension DecodableDefault { + + typealias True = Wrapper + typealias False = Wrapper + typealias EmptyString = Wrapper + typealias EmptyList = Wrapper> + typealias EmptyMap = Wrapper> + +} + +extension DecodableDefault.Wrapper: Equatable where Value: Equatable {} +extension DecodableDefault.Wrapper: Hashable where Value: Hashable {} +extension DecodableDefault.Wrapper: Encodable where Value: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(wrappedValue) + } + +} diff --git a/Sources/HPOpenWeather/DataTypes/CLLocationCoordinate+Codable.swift b/Sources/HPOpenWeather/Extensions/CLLocationCoordinate+Extensions.swift similarity index 100% rename from Sources/HPOpenWeather/DataTypes/CLLocationCoordinate+Codable.swift rename to Sources/HPOpenWeather/Extensions/CLLocationCoordinate+Extensions.swift diff --git a/Sources/HPOpenWeather/NSError+Extensions.swift b/Sources/HPOpenWeather/Extensions/NSError+Extensions.swift similarity index 59% rename from Sources/HPOpenWeather/NSError+Extensions.swift rename to Sources/HPOpenWeather/Extensions/NSError+Extensions.swift index d923c21..3252af9 100644 --- a/Sources/HPOpenWeather/NSError+Extensions.swift +++ b/Sources/HPOpenWeather/Extensions/NSError+Extensions.swift @@ -11,4 +11,7 @@ extension NSError { ) } + static let noApiKey = NSError(code: 2, description: "API key was not provided") + static let timeMachineDate = NSError(code: 3, description: "TimeMachineRequest's date has to be at least 6 hours in the past") + } diff --git a/Sources/HPOpenWeather/Requests/URLQueryItemsBuilder+Extensions.swift b/Sources/HPOpenWeather/Extensions/URLQueryItemsBuilder+Extensions.swift similarity index 100% rename from Sources/HPOpenWeather/Requests/URLQueryItemsBuilder+Extensions.swift rename to Sources/HPOpenWeather/Extensions/URLQueryItemsBuilder+Extensions.swift diff --git a/Sources/HPOpenWeather/HPOpenWeather.swift b/Sources/HPOpenWeather/OpenWeather.swift similarity index 68% rename from Sources/HPOpenWeather/HPOpenWeather.swift rename to Sources/HPOpenWeather/OpenWeather.swift index 9fd1b4e..eebcb89 100644 --- a/Sources/HPOpenWeather/HPOpenWeather.swift +++ b/Sources/HPOpenWeather/OpenWeather.swift @@ -1,13 +1,12 @@ import Foundation import HPNetwork -public final class HPOpenWeather { +public final class OpenWeather { // MARK: - Nested Types /// Type that can be used to configure all settings at once public struct Settings { - /// The OpenWeatherMap API key to authorize requests let apiKey : String let language: RequestLanguage let units: RequestUnits @@ -17,12 +16,12 @@ public final class HPOpenWeather { self.units = units self.apiKey = apiKey } - } // MARK: - Properties - public static let shared = HPOpenWeather() + /// A shared instance of the weather client + public static let shared = OpenWeather() /// The OpenWeatherMap API key to authorize requests public var apiKey : String? @@ -45,11 +44,13 @@ public final class HPOpenWeather { // MARK: - Sending Requests - @discardableResult - public func requestWeather( - _ request: R, - completion: @escaping (Result) -> Void) -> NetworkTask - { + /// <#Description#> + /// - Parameters: + /// - request: <#request description#> + /// - completion: <#completion description#> + /// - Returns: A network task that can be used to cancel the request + @discardableResult + public func sendWeatherRequest(_ request: R, completion: @escaping (Result) -> Void) -> NetworkTask { guard let apiKey = apiKey else { request.finishingQueue.async { completion(.failure(NSError.noApiKey)) @@ -70,4 +71,14 @@ public final class HPOpenWeather { } } + // MARK: - Applying Settings + + /// Applies new settings to the weather client + /// - Parameter settings: The weather client settings, including an API key, language and units + public func apply(_ settings: Settings) { + apiKey = settings.apiKey + language = settings.language + units = settings.units + } + } diff --git a/Sources/HPOpenWeather/Requests/APINetworkRequest.swift b/Sources/HPOpenWeather/Requests/APINetworkRequest.swift new file mode 100644 index 0000000..a2db817 --- /dev/null +++ b/Sources/HPOpenWeather/Requests/APINetworkRequest.swift @@ -0,0 +1,18 @@ +import Foundation +import HPNetwork + +public struct APINetworkRequest: NetworkRequest { + + public let url: URL? + public let urlSession: URLSession + public let finishingQueue: DispatchQueue + public let requestMethod: NetworkRequestMethod = .get + + public func convertResponse(response: NetworkResponse) throws -> Output { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .secondsSince1970 + + return try decoder.decode(Output.self, from: response.data) + } + +} diff --git a/Sources/HPOpenWeather/Requests/OpenWeatherRequest+Combine.swift b/Sources/HPOpenWeather/Requests/OpenWeatherRequest+Combine.swift new file mode 100644 index 0000000..be236dc --- /dev/null +++ b/Sources/HPOpenWeather/Requests/OpenWeatherRequest+Combine.swift @@ -0,0 +1,49 @@ +#if canImport(Combine) +import Combine +import Foundation + +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public extension OpenWeatherRequest { + + func publisher(apiKey: String, language: RequestLanguage = .english, units: RequestUnits = .metric) -> AnyPublisher { + let settings = OpenWeather.Settings(apiKey: apiKey, language: language, units: units) + return publisher(settings: settings) + } + + func publisher(settings: OpenWeather.Settings) -> AnyPublisher { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .secondsSince1970 + + return urlSession + .dataTaskPublisher(for: makeURL(settings: settings)) + .receive(on: finishingQueue) + .tryMap { data, response in + if let error = NetworkingError.error(from: response) { + throw error + } + return data + } + .decode(type: Output.self, decoder: decoder) + .eraseToAnyPublisher() + } + +} + +final class NetworkingError: NSError { + + static func error(from response: URLResponse?) -> Error? { + guard let response = response as? HTTPURLResponse else { + return nil + } + + switch response.statusCode { + case 200...299: + return nil + default: + let errorCode = URLError.Code(rawValue: response.statusCode) + return URLError(errorCode) + } + } + +} +#endif diff --git a/Sources/HPOpenWeather/Requests/OpenWeatherRequest.swift b/Sources/HPOpenWeather/Requests/OpenWeatherRequest.swift index 4f11f61..53a6541 100644 --- a/Sources/HPOpenWeather/Requests/OpenWeatherRequest.swift +++ b/Sources/HPOpenWeather/Requests/OpenWeatherRequest.swift @@ -5,67 +5,13 @@ import HPNetwork public protocol OpenWeatherRequest { associatedtype Output: Decodable + associatedtype Request: NetworkRequest where Request.Output == Output var coordinate: CLLocationCoordinate2D { get } var urlSession: URLSession { get } var finishingQueue: DispatchQueue { get } - func makeURL(settings: HPOpenWeather.Settings) -> URL - func makeNetworkRequest(settings: HPOpenWeather.Settings) throws -> DecodableRequest + func makeURL(settings: OpenWeather.Settings) -> URL + func makeNetworkRequest(settings: OpenWeather.Settings) throws -> Request } - -#if canImport(Combine) -import Combine - -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -public extension OpenWeatherRequest { - - func makePublisher(apiKey: String, language: RequestLanguage = .english, units: RequestUnits = .metric) -> AnyPublisher { - let settings = HPOpenWeather.Settings(apiKey: apiKey, language: language, units: units) - return makePublisher(settings: settings) - } - - func makePublisher(settings: HPOpenWeather.Settings) -> AnyPublisher { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .secondsSince1970 - - return urlSession - .dataTaskPublisher(for: makeURL(settings: settings)) - .receive(on: finishingQueue) - .tryMap { data, response in - if let error = NetworkingError.error(from: response) { - throw error - } - - return data - } - .decode(type: Output.self, decoder: decoder) - .eraseToAnyPublisher() - } - -} - -final class NetworkingError: NSError { - - static func error(from response: URLResponse?) -> Error? { - guard let response = response as? HTTPURLResponse else { - return nil - } - - switch response.statusCode { - case 200...299: - return nil - case 404: - return NSError(code: 404, description: "URL not found") - case 429: - return NSError(code: 429, description: "Too many requests") - case 401: - return NSError(code: 401, description: "Unauthorized request") - default: - return NSError(code: response.statusCode, description: "Networking returned with HTTP code \(response.statusCode)") - } - } - -} -#endif diff --git a/Sources/HPOpenWeather/Requests/RequestLanguage.swift b/Sources/HPOpenWeather/Requests/RequestLanguage.swift index eb00b2a..4c25782 100644 --- a/Sources/HPOpenWeather/Requests/RequestLanguage.swift +++ b/Sources/HPOpenWeather/Requests/RequestLanguage.swift @@ -1,5 +1,6 @@ import Foundation +/// The language that should be used in API responses for example for weather condition descriptions public enum RequestLanguage: String { case afrikaans = "af" diff --git a/Sources/HPOpenWeather/Requests/RequestUnits.swift b/Sources/HPOpenWeather/Requests/RequestUnits.swift index 4bffc9b..3308807 100644 --- a/Sources/HPOpenWeather/Requests/RequestUnits.swift +++ b/Sources/HPOpenWeather/Requests/RequestUnits.swift @@ -1,8 +1,13 @@ import Foundation +/// The units that should the data in the API responses should be formatted in public enum RequestUnits: String { + /// Temperature in Kelvin and wind speed in meter/sec + case standard + /// Temperature in Celsius and wind speed in meter/sec case metric + /// Temperature in Fahrenheit and wind speed in miles/hour case imperial } diff --git a/Sources/HPOpenWeather/Requests/TimeMachineRequest.swift b/Sources/HPOpenWeather/Requests/TimeMachineRequest.swift index 40c6bca..9ee46e2 100644 --- a/Sources/HPOpenWeather/Requests/TimeMachineRequest.swift +++ b/Sources/HPOpenWeather/Requests/TimeMachineRequest.swift @@ -4,13 +4,19 @@ import HPNetwork public struct TimeMachineRequest: OpenWeatherRequest { + // MARK: - Associated Types + public typealias Output = TimeMachineResponse + // MARK: - Properties + public let coordinate: CLLocationCoordinate2D public let date: Date public let urlSession: URLSession public let finishingQueue: DispatchQueue + // MARK: - Init + public init(coordinate: CLLocationCoordinate2D, date: Date, urlSession: URLSession = .shared, finishingQueue: DispatchQueue = .main) { self.coordinate = coordinate self.date = date @@ -18,7 +24,9 @@ public struct TimeMachineRequest: OpenWeatherRequest { self.finishingQueue = finishingQueue } - public func makeURL(settings: HPOpenWeather.Settings) -> URL { + // MARK: - OpenWeatherRequest + + public func makeURL(settings: OpenWeather.Settings) -> URL { URLQueryItemsBuilder.weatherBase .addingPathComponent("timemachine") .addingQueryItem(coordinate.latitude, digits: 5, name: "lat") @@ -30,34 +38,11 @@ public struct TimeMachineRequest: OpenWeatherRequest { .build()! } - public func makeNetworkRequest(settings: HPOpenWeather.Settings) throws -> DecodableRequest { + public func makeNetworkRequest(settings: OpenWeather.Settings) throws -> APINetworkRequest { guard date.timeIntervalSinceNow < -6 * .hour else { throw NSError.timeMachineDate } - return TimeMachineNetworkRequest(url: makeURL(settings: settings), urlSession: urlSession, finishingQueue: finishingQueue) - } - -} - -class TimeMachineNetworkRequest: DecodableRequest { - - public typealias Output = TimeMachineResponse - - public override var url: URL? { - _url - } - - public override var decoder: JSONDecoder { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .secondsSince1970 - return decoder - } - - private let _url: URL - - override init(url: URL, urlSession: URLSession, finishingQueue: DispatchQueue) { - self._url = url - super.init(urlString: "www.google.com", urlSession: urlSession, finishingQueue: finishingQueue) + return APINetworkRequest(url: makeURL(settings: settings), urlSession: urlSession, finishingQueue: finishingQueue) } } diff --git a/Sources/HPOpenWeather/Requests/WeatherRequest.swift b/Sources/HPOpenWeather/Requests/WeatherRequest.swift index 5d5340f..f50c158 100644 --- a/Sources/HPOpenWeather/Requests/WeatherRequest.swift +++ b/Sources/HPOpenWeather/Requests/WeatherRequest.swift @@ -2,28 +2,29 @@ import Foundation import CoreLocation import HPNetwork -extension NSError { - - static let noApiKey = NSError(code: 2, description: "API key was not provided") - static let timeMachineDate = NSError(code: 3, description: "TimeMachineRequest's date has to be at least 6 hours in the past") - -} - public struct WeatherRequest: OpenWeatherRequest { + // MARK: - Associated Types + public typealias Output = WeatherResponse + // MARK: - Properties + public let coordinate: CLLocationCoordinate2D public let urlSession: URLSession public let finishingQueue: DispatchQueue + // MARK: - Init + public init(coordinate: CLLocationCoordinate2D, urlSession: URLSession = .shared, finishingQueue: DispatchQueue = .main) { self.coordinate = coordinate self.urlSession = urlSession self.finishingQueue = finishingQueue } - public func makeURL(settings: HPOpenWeather.Settings) -> URL { + // MARK: - OpenWeatherRequest + + public func makeURL(settings: OpenWeather.Settings) -> URL { URLQueryItemsBuilder(host: "api.openweathermap.org") .addingPathComponent("data") .addingPathComponent("2.5") @@ -36,36 +37,13 @@ public struct WeatherRequest: OpenWeatherRequest { .build()! } - public func makeNetworkRequest(settings: HPOpenWeather.Settings) throws -> DecodableRequest { - WeatherNetworkRequest(url: makeURL(settings: settings), urlSession: urlSession, finishingQueue: finishingQueue) + public func makeNetworkRequest(settings: OpenWeather.Settings) throws -> APINetworkRequest { + APINetworkRequest(url: makeURL(settings: settings), urlSession: urlSession, finishingQueue: finishingQueue) } } -class WeatherNetworkRequest: DecodableRequest { - - public typealias Output = WeatherResponse - - override var url: URL? { - _url - } - - public override var decoder: JSONDecoder { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .secondsSince1970 - return decoder - } - - private let _url: URL - - override init(url: URL, urlSession: URLSession, finishingQueue: DispatchQueue) { - self._url = url - super.init(urlString: "www.google.com", urlSession: urlSession, finishingQueue: finishingQueue) - } - -} - -extension Double { +extension TimeInterval { static let minute = 60.00 static let hour = 3600.00 diff --git a/Sources/HPOpenWeather/Response/TimeMachineResponse.swift b/Sources/HPOpenWeather/Response/TimeMachineResponse.swift index 2c01da1..9fae2f4 100644 --- a/Sources/HPOpenWeather/Response/TimeMachineResponse.swift +++ b/Sources/HPOpenWeather/Response/TimeMachineResponse.swift @@ -2,35 +2,18 @@ import Foundation public struct TimeMachineResponse: Codable, Equatable, Hashable { - public let timezone: TimeZone + private let timezoneIdentifier: String public let current: CurrentWeather - public let hourlyForecasts: [HourlyForecast] + @DecodableDefault.EmptyList public var hourlyForecasts: [HourlyForecast] - enum CodingKeys: String, CodingKey { - case timezone - case current - case hourly - } + public var timezone: TimeZone { + TimeZone(identifier: timezoneIdentifier)! + } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let timezoneString = try container.decode(String.self, forKey: .timezone) - guard let timezone = TimeZone(identifier: timezoneString) else { - throw NSError(code: 4, description: "Invalid timezone identifier in weather response") - } - - self.timezone = timezone - self.current = try container.decode(CurrentWeather.self, forKey: .current) - self.hourlyForecasts = try container.decode([HourlyForecast].self, forKey: .hourly) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(timezone.identifier, forKey: .timezone) - try container.encode(current, forKey: .current) - try container.encode(hourlyForecasts, forKey: .hourly) - } + enum CodingKeys: String, CodingKey { + case timezoneIdentifier = "timezone" + case current + case hourlyForecasts = "hourly" + } } diff --git a/Sources/HPOpenWeather/Response/WeatherResponse.swift b/Sources/HPOpenWeather/Response/WeatherResponse.swift index 5c90e69..c7e8f59 100644 --- a/Sources/HPOpenWeather/Response/WeatherResponse.swift +++ b/Sources/HPOpenWeather/Response/WeatherResponse.swift @@ -2,39 +2,23 @@ import Foundation public struct WeatherResponse: Codable, Equatable, Hashable { - public let timezone: TimeZone + private let timezoneIdentifier: String public let current: CurrentWeather - public let hourlyForecasts: [HourlyForecast] - public let dailyForecasts: [DailyForecast] + @DecodableDefault.EmptyList public var hourlyForecasts: [HourlyForecast] + @DecodableDefault.EmptyList public var dailyForecasts: [DailyForecast] + /// Government weather alerts data from major national weather warning systems + @DecodableDefault.EmptyList public var alerts: [Alert] + + public var timezone: TimeZone { + TimeZone(identifier: timezoneIdentifier)! + } enum CodingKeys: String, CodingKey { - case timezone + case timezoneIdentifier = "timezone" case current - case hourly - case daily - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let timezoneString = try container.decode(String.self, forKey: .timezone) - guard let timezone = TimeZone(identifier: timezoneString) else { - throw NSError(code: 4, description: "Invalid timezone identifier in weather response") - } - - self.timezone = timezone - self.current = try container.decode(CurrentWeather.self, forKey: .current) - self.hourlyForecasts = try container.decode([HourlyForecast].self, forKey: .hourly) - self.dailyForecasts = try container.decode([DailyForecast].self, forKey: .daily) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(timezone.identifier, forKey: .timezone) - try container.encode(current, forKey: .current) - try container.encode(hourlyForecasts, forKey: .hourly) - try container.encode(dailyForecasts, forKey: .daily) + case hourlyForecasts = "hourly" + case dailyForecasts = "daily" + case alerts } } diff --git a/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift b/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift index 060d560..45704e2 100644 --- a/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift +++ b/Tests/HPOpenWeatherTests/HPOpenWeatherTests.swift @@ -6,21 +6,19 @@ final class HPOpenWeatherTests: XCTestCase { override class func setUp() { super.setUp() - - HPOpenWeather.shared.apiKey = TestSecret.apiKey + OpenWeather.shared.apiKey = TestSecret.apiKey } override class func tearDown() { super.tearDown() - - HPOpenWeather.shared.apiKey = nil + OpenWeather.shared.apiKey = nil } func testCurrentRequest() { let request = WeatherRequest(coordinate: .init(latitude: 40, longitude: 30)) let exp = XCTestExpectation(description: "Fetched data") - HPOpenWeather.shared.requestWeather(request) { result in + OpenWeather.shared.sendWeatherRequest(request) { result in exp.fulfill() XCTAssertResult(result) } @@ -32,7 +30,7 @@ final class HPOpenWeatherTests: XCTestCase { let request = TimeMachineRequest(coordinate: .init(latitude: 40, longitude: 30), date: Date().addingTimeInterval(-1 * .hour)) let exp = XCTestExpectation(description: "Fetched data") - HPOpenWeather.shared.requestWeather(request) { result in + OpenWeather.shared.sendWeatherRequest(request) { result in exp.fulfill() XCTAssertResultError(result) } @@ -44,7 +42,7 @@ final class HPOpenWeatherTests: XCTestCase { let request = TimeMachineRequest(coordinate: .init(latitude: 40, longitude: 30), date: Date().addingTimeInterval(-7 * .hour)) let exp = XCTestExpectation(description: "Fetched data") - HPOpenWeather.shared.requestWeather(request) { result in + OpenWeather.shared.sendWeatherRequest(request) { result in exp.fulfill() XCTAssertResult(result) } @@ -61,7 +59,7 @@ final class HPOpenWeatherTests: XCTestCase { let expectationReceive = expectation(description: "receiveValue") //let expectationFailure = expectation(description: "failure") - let cancellable = request.makePublisher(apiKey: TestSecret.apiKey).sink( + let cancellable = request.publisher(apiKey: TestSecret.apiKey).sink( receiveCompletion: { result in switch result { case .failure(let error): @@ -98,6 +96,7 @@ extension Encodable { /// Asserts that the result is not a failure func XCTAssertResult(_ result: Result) { if case .failure(let error as NSError) = result { + print(error) XCTFail(error.localizedDescription) } } diff --git a/Tests/HPOpenWeatherTests/WeatherIconTests.swift b/Tests/HPOpenWeatherTests/WeatherIconTests.swift new file mode 100644 index 0000000..b72eb65 --- /dev/null +++ b/Tests/HPOpenWeatherTests/WeatherIconTests.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import HPOpenWeather + +final class WeatherIconTests: XCTestCase { + + #if canImport(UIKit) + @available(iOS 13.0, tvOS 13.0, *) + func testAllSystemImages() { + WeatherIcon.allCases.forEach { + XCTAssertNotNil($0.filledUIImage()) + } + } + #endif + +}