diff --git a/SwiftyForecast Widget/Provider/WeatherProvider.swift b/SwiftyForecast Widget/Provider/WeatherProvider.swift new file mode 100644 index 00000000..2c746ff4 --- /dev/null +++ b/SwiftyForecast Widget/Provider/WeatherProvider.swift @@ -0,0 +1,68 @@ +// +// WeatherProvider.swift +// SwiftyForecast +// +// Created by Pawel Milek on 12/8/23. +// Copyright © 2023 Pawel Milek. All rights reserved. +// + +import WidgetKit +import SwiftUI +import CoreLocation + +struct WeatherProvider: TimelineProvider { + private let locationManager = WidgetLocationManager() + private let dataSource: WeatherProviderDataSource + + init(dataSource: WeatherProviderDataSource = WeatherProviderDataSource()) { + self.dataSource = dataSource + } + + func placeholder(in context: Context) -> WeatherEntry { + WeatherEntry.sampleTimeline.first! + } + + func getSnapshot(in context: Context, completion: @escaping (WeatherEntry) -> Void) { + Task { + let result = await loadWeatherDataForCurrentLocation() + let now = Date.now + + let entry = WeatherEntry( + date: now, + locationName: result.name, + icon: result.icon, + description: result.description, + temperature: result.temperature, + temperatureMaxMin: result.temperatureMaxMin + ) + + completion(entry) + } + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + Task { + let result = await loadWeatherDataForCurrentLocation() + let now = Date.now + + let entry = WeatherEntry( + date: now, + locationName: result.name, + icon: result.icon, + description: result.description, + temperature: result.temperature, + temperatureMaxMin: result.temperatureMaxMin + ) + + let nextUpdate = Calendar.current.date(byAdding: .minute, value: 45, to: now)! + let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) + completion(timeline) + } + } + + private func loadWeatherDataForCurrentLocation() async -> WeatherProviderDataSource.EntryData { + let location = await locationManager.startUpdatingLocation() + let result = await dataSource.loadEntryData(for: location) + return result + } +} diff --git a/SwiftyForecast Widget/Provider/WeatherProviderDataSource.swift b/SwiftyForecast Widget/Provider/WeatherProviderDataSource.swift new file mode 100644 index 00000000..5dda9b8c --- /dev/null +++ b/SwiftyForecast Widget/Provider/WeatherProviderDataSource.swift @@ -0,0 +1,84 @@ +// +// WeatherProviderDataSource.swift +// SwiftyForecastWidgetExtension +// +// Created by Pawel Milek on 12/13/23. +// Copyright © 2023 Pawel Milek. All rights reserved. +// + +import Foundation +import SwiftUI +import CoreLocation + +struct WeatherProviderDataSource { + struct EntryData { + let name: String + let icon: Image + let description: String + let temperature: String + let temperatureMaxMin: String + } + + private let service: WeatherServiceProtocol + private let temperatureRenderer: TemperatureRenderer + + init( + service: WeatherServiceProtocol = WeatherService(), + temperatureRenderer: TemperatureRenderer = TemperatureRenderer() + ) { + self.service = service + self.temperatureRenderer = temperatureRenderer + } + + func loadEntryData(for location: CLLocation) async -> EntryData { + let placemark = await fetchPlacemark(at: location) + let name = placemark.locality ?? InvalidReference.undefined + let coordinate = placemark.location?.coordinate ?? location.coordinate + let model = await fetchCurrentWeather(coordinate: coordinate) + let icon = await fetchIcon(with: model.icon) + + let readyForDisplayTemperature = temperatureRenderer.render(model.temperatureValue) + + let result = EntryData( + name: name, + icon: icon, + description: model.description, + temperature: readyForDisplayTemperature.current, + temperatureMaxMin: readyForDisplayTemperature.maxMin + ) + + return result + } + + private func fetchPlacemark(at location: CLLocation) async -> CLPlacemark { + do { + let placemark = try await Geocoder.fetchPlacemark(at: location) + return placemark + } catch { + fatalError(error.localizedDescription) + } + } + + private func fetchCurrentWeather(coordinate: CLLocationCoordinate2D) async -> CurrentWeatherModel { + do { + let response = try await service.fetchCurrent( + latitude: coordinate.latitude, + longitude: coordinate.longitude + ) + let model = ResponseParser.parse(current: response) + return model + } catch { + fatalError(error.localizedDescription) + } + } + + private func fetchIcon(with symbol: String) async -> Image { + do { + let result = try await service.fetchLargeIcon(symbol: symbol) + let image = Image(uiImage: result) + return image + } catch { + fatalError(error.localizedDescription) + } + } +} diff --git a/SwiftyForecast Widget/WeatherEntry.swift b/SwiftyForecast Widget/WeatherEntry.swift index 8f6ab0f1..15b3dde0 100644 --- a/SwiftyForecast Widget/WeatherEntry.swift +++ b/SwiftyForecast Widget/WeatherEntry.swift @@ -11,26 +11,26 @@ import SwiftUI struct WeatherEntry: TimelineEntry { let date: Date + let locationName: String let icon: Image + let description: String let temperature: String let temperatureMaxMin: String - let locationName: String - let description: String init( date: Date, + locationName: String, icon: Image, + description: String, temperature: String, - temperatureMaxMin: String, - locationName: String, - description: String + temperatureMaxMin: String ) { self.date = date + self.locationName = locationName self.icon = icon + self.description = description self.temperature = temperature self.temperatureMaxMin = temperatureMaxMin - self.locationName = locationName - self.description = description } } @@ -38,19 +38,19 @@ extension WeatherEntry { static let sampleTimeline = [ WeatherEntry( date: Date(), + locationName: "Cupertino", icon: Image(.cloudySky), + description: "light intensity shower rain", temperature: "69°", - temperatureMaxMin: "⏶ 75° ⏷ 72°", - locationName: "Cupertino", - description: "light intensity shower rain" + temperatureMaxMin: "⏶ 75° ⏷ 72°" ), WeatherEntry( date: Date(), + locationName: "Cupertino", icon: Image(.clearSky), + description: "scattered clouds", temperature: "87°", - temperatureMaxMin: "⏶ 92° ⏷ 45°", - locationName: "Cupertino", - description: "scattered clouds" + temperatureMaxMin: "⏶ 92° ⏷ 45°" ) ] } diff --git a/SwiftyForecast Widget/WeatherProvider.swift b/SwiftyForecast Widget/WeatherProvider.swift deleted file mode 100644 index 17354826..00000000 --- a/SwiftyForecast Widget/WeatherProvider.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// WeatherProvider.swift -// SwiftyForecast -// -// Created by Pawel Milek on 12/8/23. -// Copyright © 2023 Pawel Milek. All rights reserved. -// - -import WidgetKit -import SwiftUI -import CoreLocation - -struct WeatherProvider: TimelineProvider { - private let locationManager = WidgetLocationManager() - private let service: WeatherServiceProtocol - private let notationController: NotationController - private let temperatureFormatterFactory: TemperatureFormatterFactoryProtocol - - init( - service: WeatherServiceProtocol = WeatherService(), - notationController: NotationController = NotationController(), - temperatureFormatterFactory: TemperatureFormatterFactoryProtocol = TemperatureFormatterFactory() - ) { - self.service = service - self.notationController = notationController - self.temperatureFormatterFactory = temperatureFormatterFactory - } - - func placeholder(in context: Context) -> WeatherEntry { - WeatherEntry.sampleTimeline.first! - } - - func getSnapshot(in context: Context, completion: @escaping (WeatherEntry) -> Void) { - Task { - let (name, model, icon) = await loadWeatherData() - - let values = TemperatureValue( - current: model.temperature, - max: model.maxTemperature, - min: model.minTemperature - ) - let temp: (current: String, maxMin: String) = temperatureFormatted(valueInKelvin: values) - let now = Date.now - - let entry = WeatherEntry( - date: now, - icon: icon, - temperature: temp.current, - temperatureMaxMin: temp.maxMin, - locationName: name, - description: model.description - ) - - completion(entry) - } - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { - Task { - let (name, model, icon) = await loadWeatherData() - - let values = TemperatureValue( - current: model.temperature, - max: model.maxTemperature, - min: model.minTemperature - ) - - let (current, maxMin) = temperatureFormatted(valueInKelvin: values) - - let now = Date.now - let nextUpdate = Calendar.current.date(byAdding: .minute, value: 45, to: now)! - let entry = WeatherEntry( - date: now, - icon: icon, - temperature: current, - temperatureMaxMin: maxMin, - locationName: name, - description: model.description - ) - - let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) - completion(timeline) - } - } - - private func loadWeatherData() async -> (String, CurrentWeatherModel, Image) { - let location = await locationManager.startUpdatingLocation() - let placemark = await fetchPlacemark(at: location) - - let name = placemark.locality ?? InvalidReference.undefined - let coordinate = placemark.location?.coordinate ?? location.coordinate - let model = await fetchCurrentWeather(coordinate: coordinate) - let icon = await fetchIcon(with: model.icon) - return (name, model, icon) - } - - private func fetchPlacemark(at location: CLLocation) async -> CLPlacemark { - do { - let placemark = try await Geocoder.fetchPlacemark(at: location) - return placemark - } catch { - fatalError(error.localizedDescription) - } - } - - private func fetchCurrentWeather(coordinate: CLLocationCoordinate2D) async -> CurrentWeatherModel { - do { - let response = try await service.fetchCurrent( - latitude: coordinate.latitude, - longitude: coordinate.longitude - ) - let model = ResponseParser.parse(current: response) - return model - } catch { - fatalError(error.localizedDescription) - } - } - - private func fetchIcon(with symbol: String) async -> Image { - do { - let result = try await service.fetchLargeIcon(symbol: symbol) - let image = Image(uiImage: result) - return image - } catch { - fatalError(error.localizedDescription) - } - } - - private func temperatureFormatted(valueInKelvin source: TemperatureValue) -> (current: String, maxMin: String) { - let temperatureFormatter = temperatureFormatterFactory.make( - by: notationController.temperatureNotation, - valueInKelvin: source - ) - - let temperature = temperatureFormatter.currentFormatted - let maxTemp = temperatureFormatter.maxFormatted - let minTemp = temperatureFormatter.minFormatted - let temperatureMaxMin = "⏶ \(maxTemp) ⏷ \(minTemp)" - return (current: temperature, maxMin: temperatureMaxMin) - } -} diff --git a/SwiftyForecast.xcodeproj/project.pbxproj b/SwiftyForecast.xcodeproj/project.pbxproj index 924d5757..94f89a22 100644 --- a/SwiftyForecast.xcodeproj/project.pbxproj +++ b/SwiftyForecast.xcodeproj/project.pbxproj @@ -11,6 +11,10 @@ E2092ABF2B0CB40700F38CF9 /* LocationList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2092ABE2B0CB40700F38CF9 /* LocationList.swift */; }; E20DA45B214348AD0047491B /* NetworkReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20DA45A214348AD0047491B /* NetworkReachabilityManager.swift */; }; E20DA45F21434ACB0047491B /* ReachabilityError+ErrorHandleable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20DA45E21434ACB0047491B /* ReachabilityError+ErrorHandleable.swift */; }; + E2117B462B29B9F500F91ADB /* WeatherProviderDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2117B452B29B9F500F91ADB /* WeatherProviderDataSource.swift */; }; + E2117B482B29D48300F91ADB /* TemperatureRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2117B472B29D48300F91ADB /* TemperatureRenderer.swift */; }; + E2117B492B29D48E00F91ADB /* TemperatureRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2117B472B29D48300F91ADB /* TemperatureRenderer.swift */; }; + E2117B4B2B29D69700F91ADB /* SpeedRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2117B4A2B29D69700F91ADB /* SpeedRenderer.swift */; }; E2192259214483AA0093FEBD /* OfflineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2192258214483AA0093FEBD /* OfflineViewController.swift */; }; E21ABD062AE5A0DF00F93F01 /* LocationWeatherView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21ABD002AE5A0DF00F93F01 /* LocationWeatherView+ViewModel.swift */; }; E21ABD072AE5A0DF00F93F01 /* LocationSearchCompleter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21ABD022AE5A0DF00F93F01 /* LocationSearchCompleter.swift */; }; @@ -230,6 +234,9 @@ E2092ABE2B0CB40700F38CF9 /* LocationList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationList.swift; sourceTree = ""; }; E20DA45A214348AD0047491B /* NetworkReachabilityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkReachabilityManager.swift; sourceTree = ""; }; E20DA45E21434ACB0047491B /* ReachabilityError+ErrorHandleable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReachabilityError+ErrorHandleable.swift"; sourceTree = ""; }; + E2117B452B29B9F500F91ADB /* WeatherProviderDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherProviderDataSource.swift; sourceTree = ""; }; + E2117B472B29D48300F91ADB /* TemperatureRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureRenderer.swift; sourceTree = ""; }; + E2117B4A2B29D69700F91ADB /* SpeedRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedRenderer.swift; sourceTree = ""; }; E2192258214483AA0093FEBD /* OfflineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineViewController.swift; sourceTree = ""; }; E21ABD002AE5A0DF00F93F01 /* LocationWeatherView+ViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LocationWeatherView+ViewModel.swift"; sourceTree = ""; }; E21ABD022AE5A0DF00F93F01 /* LocationSearchCompleter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationSearchCompleter.swift; sourceTree = ""; }; @@ -419,6 +426,15 @@ name = Frameworks; sourceTree = ""; }; + E2117B442B29B9E200F91ADB /* Provider */ = { + isa = PBXGroup; + children = ( + E2F671202B23682400B791A0 /* WeatherProvider.swift */, + E2117B452B29B9F500F91ADB /* WeatherProviderDataSource.swift */, + ); + path = Provider; + sourceTree = ""; + }; E21A0B9420EE88F5006AC409 /* Utilities */ = { isa = PBXGroup; children = ( @@ -461,7 +477,6 @@ E2227E852ADD54AD000C3886 /* TemperatureFormatter */ = { isa = PBXGroup; children = ( - E21ABD4B2AE93E6E00F93F01 /* TemperatureValue.swift */, E2227E882ADD54C7000C3886 /* TemperatureCelsiusFormatter.swift */, E2227E8A2ADD54D7000C3886 /* TemperatureFahrenheitFormatter.swift */, E2227E862ADD54B8000C3886 /* TemperatureValueDisplayable.swift */, @@ -507,6 +522,7 @@ E23997DA20E84104007D8CE4 /* Models */ = { isa = PBXGroup; children = ( + E21ABD4B2AE93E6E00F93F01 /* TemperatureValue.swift */, E2B035722500881A00DC0E77 /* CurrentWeatherModel.swift */, E26ACD1A24FF204000AC15A6 /* ForecastWeatherModel.swift */, E2B0357A2500885100DC0E77 /* DailyForecastModel.swift */, @@ -594,6 +610,8 @@ E2854FFD2290F24800CD7554 /* TemperatureNotation.swift */, E28550012290F26E00CD7554 /* MeasurementSystem.swift */, E2FEB7BC2B172782009D54B2 /* MeasurementSystemNotification.swift */, + E2117B472B29D48300F91ADB /* TemperatureRenderer.swift */, + E2117B4A2B29D69700F91ADB /* SpeedRenderer.swift */, E2227E8C2ADD5DB8000C3886 /* SpeedFormatter */, E2227E852ADD54AD000C3886 /* TemperatureFormatter */, ); @@ -867,7 +885,7 @@ children = ( E2FDBD6A2B2079DF0012D7C4 /* SwiftyForecastWidgetBundle.swift */, E2F6711E2B23680800B791A0 /* CurrentWeatherView.swift */, - E2F671202B23682400B791A0 /* WeatherProvider.swift */, + E2117B442B29B9E200F91ADB /* Provider */, E2FDBD6C2B2079DF0012D7C4 /* SwiftyForecastWidget.swift */, E2F671242B2368F700B791A0 /* WeatherEntry.swift */, E2FDBD6E2B2079E00012D7C4 /* Assets.xcassets */, @@ -1138,11 +1156,13 @@ E23997ED20E84371007D8CE4 /* JSONParser.swift in Sources */, E25EF00B20F97CE200E4C115 /* Style.swift in Sources */, E21ABD462AE8301A00F93F01 /* LocationRow.swift in Sources */, + E2117B4B2B29D69700F91ADB /* SpeedRenderer.swift in Sources */, E21ABD062AE5A0DF00F93F01 /* LocationWeatherView+ViewModel.swift in Sources */, E2FEB7B32B15F92E009D54B2 /* Humidity.swift in Sources */, E28092E82AFD0A410019FD7C /* LocationSearchViewController.swift in Sources */, E28550022290F26E00CD7554 /* MeasurementSystem.swift in Sources */, E20DA45B214348AD0047491B /* NetworkReachabilityManager.swift in Sources */, + E2117B482B29D48300F91ADB /* TemperatureRenderer.swift in Sources */, E2FEB7B72B15FA18009D54B2 /* CurrentWeatherCardViewController.swift in Sources */, E2227E872ADD54B8000C3886 /* TemperatureValueDisplayable.swift in Sources */, E2A32F822544D389000ACEFE /* ReviewNotificationCenter.swift in Sources */, @@ -1218,6 +1238,7 @@ buildActionMask = 2147483647; files = ( E2FDBD6D2B2079DF0012D7C4 /* SwiftyForecastWidget.swift in Sources */, + E2117B462B29B9F500F91ADB /* WeatherProviderDataSource.swift in Sources */, E283572B2B23894800E3807A /* Geocoder.swift in Sources */, E2AD326A2B23774400F46FBE /* URLRequest+Parameters.swift in Sources */, E2F671232B23682F00B791A0 /* WeatherProvider.swift in Sources */, @@ -1233,6 +1254,7 @@ E2AD326B2B23774400F46FBE /* Endpoint.swift in Sources */, E2AD32692B23774400F46FBE /* CurrentWeatherResponse.swift in Sources */, E2AD32682B23774400F46FBE /* WeatherEndpoint.swift in Sources */, + E2117B492B29D48E00F91ADB /* TemperatureRenderer.swift in Sources */, E2F671162B234EAE00B791A0 /* TemperatureValueDisplayable.swift in Sources */, E2F6711A2B2350AD00B791A0 /* TemperatureCelsiusFormatter.swift in Sources */, E2AD32702B23779000F46FBE /* ConfigurationSettingsAccessor.swift in Sources */, diff --git a/SwiftyForecast/Measurement/SpeedFormatter/SpeedFormatterImperial.swift b/SwiftyForecast/Measurement/SpeedFormatter/SpeedFormatterImperial.swift index 8eb4aa49..9a576f47 100644 --- a/SwiftyForecast/Measurement/SpeedFormatter/SpeedFormatterImperial.swift +++ b/SwiftyForecast/Measurement/SpeedFormatter/SpeedFormatterImperial.swift @@ -9,7 +9,7 @@ import Foundation struct SpeedFormatterImperial: SpeedValueDisplayable { - var current: String { + var currentFormatted: String { let metersPerSecond = Measurement(value: valueMetersPerSecond, unit: UnitSpeed.metersPerSecond) let converted = metersPerSecond.converted(to: .milesPerHour) return formantted(converted) diff --git a/SwiftyForecast/Measurement/SpeedFormatter/SpeedFormatterMetric.swift b/SwiftyForecast/Measurement/SpeedFormatter/SpeedFormatterMetric.swift index 73601351..3d9d793a 100644 --- a/SwiftyForecast/Measurement/SpeedFormatter/SpeedFormatterMetric.swift +++ b/SwiftyForecast/Measurement/SpeedFormatter/SpeedFormatterMetric.swift @@ -9,7 +9,7 @@ import Foundation struct SpeedFormatterMetric: SpeedValueDisplayable { - var current: String { + var currentFormatted: String { let metersPerSecond = Measurement(value: valueMetersPerSecond, unit: UnitSpeed.metersPerSecond) return formantted(metersPerSecond) } diff --git a/SwiftyForecast/Measurement/SpeedFormatter/SpeedValueDisplayable.swift b/SwiftyForecast/Measurement/SpeedFormatter/SpeedValueDisplayable.swift index b3c76873..2a6270a4 100644 --- a/SwiftyForecast/Measurement/SpeedFormatter/SpeedValueDisplayable.swift +++ b/SwiftyForecast/Measurement/SpeedFormatter/SpeedValueDisplayable.swift @@ -9,7 +9,7 @@ import Foundation protocol SpeedValueDisplayable { - var current: String { get } + var currentFormatted: String { get } } extension SpeedValueDisplayable { diff --git a/SwiftyForecast/Measurement/SpeedRenderer.swift b/SwiftyForecast/Measurement/SpeedRenderer.swift new file mode 100644 index 00000000..9eedcb77 --- /dev/null +++ b/SwiftyForecast/Measurement/SpeedRenderer.swift @@ -0,0 +1,31 @@ +// +// SpeedRenderer.swift +// SwiftyForecast +// +// Created by Pawel Milek on 12/13/23. +// Copyright © 2023 Pawel Milek. All rights reserved. +// + +import Foundation + +struct SpeedRenderer { + private let notationController: NotationController + private let speedFormatterFactory: SpeedFormatterFactoryProtocol + + init( + notationController: NotationController = NotationController(), + speedFormatterFactory: SpeedFormatterFactoryProtocol = SpeedFormatterFactory() + ) { + self.notationController = notationController + self.speedFormatterFactory = speedFormatterFactory + } + + func render(_ source: Double) -> String { + let value = speedFormatterFactory.make( + by: notationController.measurementSystem, + valueInMetersPerSec: source + ) + + return value.currentFormatted + } +} diff --git a/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureCelsiusFormatter.swift b/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureCelsiusFormatter.swift index a13fb06e..8d4913b8 100644 --- a/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureCelsiusFormatter.swift +++ b/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureCelsiusFormatter.swift @@ -13,10 +13,10 @@ struct TemperatureCelsiusFormatter: TemperatureValueDisplayable { private let maxInKelvin: Double private let minInKelvin: Double - init(currentInKelvin: Double, maxInKelvin: Double, minInKelvin: Double) { + init(currentInKelvin: Double, minInKelvin: Double, maxInKelvin: Double) { self.currentInKelvin = currentInKelvin - self.maxInKelvin = maxInKelvin self.minInKelvin = minInKelvin + self.maxInKelvin = maxInKelvin } var currentFormatted: String { @@ -25,14 +25,14 @@ struct TemperatureCelsiusFormatter: TemperatureValueDisplayable { return result } - var maxFormatted: String { - let temperature = toCelsius(maxInKelvin) + var minFormatted: String { + let temperature = toCelsius(minInKelvin) let result = formantted(temperature) return result } - var minFormatted: String { - let temperature = toCelsius(minInKelvin) + var maxFormatted: String { + let temperature = toCelsius(maxInKelvin) let result = formantted(temperature) return result } @@ -42,13 +42,13 @@ struct TemperatureCelsiusFormatter: TemperatureValueDisplayable { return Int(temperature.value) } - var maxValue: Int { - let temperature = toCelsius(maxInKelvin) + var minValue: Int { + let temperature = toCelsius(minInKelvin) return Int(temperature.value) } - var minValue: Int { - let temperature = toCelsius(minInKelvin) + var maxValue: Int { + let temperature = toCelsius(maxInKelvin) return Int(temperature.value) } diff --git a/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureFahrenheitFormatter.swift b/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureFahrenheitFormatter.swift index 9d21d219..c6e6d2bb 100644 --- a/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureFahrenheitFormatter.swift +++ b/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureFahrenheitFormatter.swift @@ -10,13 +10,13 @@ import Foundation struct TemperatureFahrenheitFormatter: TemperatureValueDisplayable { private let currentInKelvin: Double - private let maxInKelvin: Double private let minInKelvin: Double + private let maxInKelvin: Double - init(currentInKelvin: Double, maxInKelvin: Double, minInKelvin: Double) { + init(currentInKelvin: Double, minInKelvin: Double, maxInKelvin: Double) { self.currentInKelvin = currentInKelvin - self.maxInKelvin = maxInKelvin self.minInKelvin = minInKelvin + self.maxInKelvin = maxInKelvin } var currentFormatted: String { @@ -25,14 +25,14 @@ struct TemperatureFahrenheitFormatter: TemperatureValueDisplayable { return result } - var maxFormatted: String { - let temperature = toFahrenheit(maxInKelvin) + var minFormatted: String { + let temperature = toFahrenheit(minInKelvin) let result = formantted(temperature) return result } - var minFormatted: String { - let temperature = toFahrenheit(minInKelvin) + var maxFormatted: String { + let temperature = toFahrenheit(maxInKelvin) let result = formantted(temperature) return result } @@ -42,13 +42,13 @@ struct TemperatureFahrenheitFormatter: TemperatureValueDisplayable { return Int(temperature.value) } - var maxValue: Int { - let temperature = toFahrenheit(maxInKelvin) + var minValue: Int { + let temperature = toFahrenheit(minInKelvin) return Int(temperature.value) } - var minValue: Int { - let temperature = toFahrenheit(minInKelvin) + var maxValue: Int { + let temperature = toFahrenheit(maxInKelvin) return Int(temperature.value) } diff --git a/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureFormatterFactory.swift b/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureFormatterFactory.swift index 780b6ad5..26526ef3 100644 --- a/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureFormatterFactory.swift +++ b/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureFormatterFactory.swift @@ -26,7 +26,7 @@ struct TemperatureFormatterFactory: TemperatureFormatterFactoryProtocol { by notation: TemperatureNotation, valueInKelvin current: Double ) -> TemperatureValueDisplayable { - let value = TemperatureValue(current: current, max: .signalingNaN, min: .signalingNaN) + let value = TemperatureValue(current: current, min: .signalingNaN, max: .signalingNaN) return make(by: notation, valueInKelvin: value) } @@ -38,15 +38,15 @@ struct TemperatureFormatterFactory: TemperatureFormatterFactoryProtocol { case .celsius: return TemperatureCelsiusFormatter( currentInKelvin: valueInKelvin.current, - maxInKelvin: valueInKelvin.max, - minInKelvin: valueInKelvin.min + minInKelvin: valueInKelvin.min, + maxInKelvin: valueInKelvin.max ) case .fahrenheit: return TemperatureFahrenheitFormatter( currentInKelvin: valueInKelvin.current, - maxInKelvin: valueInKelvin.max, - minInKelvin: valueInKelvin.min + minInKelvin: valueInKelvin.min, + maxInKelvin: valueInKelvin.max ) } } diff --git a/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureValueDisplayable.swift b/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureValueDisplayable.swift index 13288598..b1c1f912 100644 --- a/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureValueDisplayable.swift +++ b/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureValueDisplayable.swift @@ -10,12 +10,13 @@ import Foundation protocol TemperatureValueDisplayable { var currentFormatted: String { get } - var maxFormatted: String { get } var minFormatted: String { get } + var maxFormatted: String { get } var currentValue: Int { get } - var maxValue: Int { get } var minValue: Int { get } + var maxValue: Int { get } + } extension TemperatureValueDisplayable { diff --git a/SwiftyForecast/Measurement/TemperatureRenderer.swift b/SwiftyForecast/Measurement/TemperatureRenderer.swift new file mode 100644 index 00000000..37f02219 --- /dev/null +++ b/SwiftyForecast/Measurement/TemperatureRenderer.swift @@ -0,0 +1,55 @@ +// +// TemperatureRenderer.swift +// SwiftyForecast +// +// Created by Pawel Milek on 12/13/23. +// Copyright © 2023 Pawel Milek. All rights reserved. +// + +import Foundation + +struct TemperatureRenderer { + struct ReadyForDisplay { + let currentValue: Int + let current: String + let maxMin: String + + } + + private let notationController: NotationController + private let temperatureFormatterFactory: TemperatureFormatterFactoryProtocol + + init( + notationController: NotationController = NotationController(), + temperatureFormatterFactory: TemperatureFormatterFactoryProtocol = TemperatureFormatterFactory() + ) { + self.notationController = notationController + self.temperatureFormatterFactory = temperatureFormatterFactory + } + + func render(_ source: TemperatureValue) -> ReadyForDisplay { + let value = temperatureFormatterFactory.make( + by: notationController.temperatureNotation, + valueInKelvin: source + ) + + return ReadyForDisplay( + currentValue: value.currentValue, + current: value.currentFormatted, + maxMin: "⏶ \(value.maxFormatted) ⏷ \(value.minFormatted)" + ) + } + + func render(_ source: Double) -> ReadyForDisplay { + let value = temperatureFormatterFactory.make( + by: notationController.temperatureNotation, + valueInKelvin: source + ) + + return ReadyForDisplay( + currentValue: value.currentValue, + current: value.currentFormatted, + maxMin: "" + ) + } +} diff --git a/SwiftyForecast/Models/CurrentWeatherModel.swift b/SwiftyForecast/Models/CurrentWeatherModel.swift index 7d1626c5..e7f2bb81 100644 --- a/SwiftyForecast/Models/CurrentWeatherModel.swift +++ b/SwiftyForecast/Models/CurrentWeatherModel.swift @@ -11,9 +11,7 @@ import Foundation struct CurrentWeatherModel { let date: Date let dayNightState: DayNightState - let temperature: Double - let maxTemperature: Double - let minTemperature: Double + let temperatureValue: TemperatureValue let description: String let icon: String let humidity: Int @@ -27,9 +25,7 @@ extension CurrentWeatherModel { static let example = CurrentWeatherModel( date: Date(timeIntervalSinceReferenceDate: 724103328.0), dayNightState: .night, - temperature: 276.14, - maxTemperature: 276.14, - minTemperature: 275.55, + temperatureValue: TemperatureValue(current: 276.14, min: 275.55, max: 276.14), description: "clear sky", icon: "01n", humidity: 87, diff --git a/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureValue.swift b/SwiftyForecast/Models/TemperatureValue.swift similarity index 100% rename from SwiftyForecast/Measurement/TemperatureFormatter/TemperatureValue.swift rename to SwiftyForecast/Models/TemperatureValue.swift index b6faa3b1..99a46c74 100644 --- a/SwiftyForecast/Measurement/TemperatureFormatter/TemperatureValue.swift +++ b/SwiftyForecast/Models/TemperatureValue.swift @@ -10,6 +10,6 @@ import Foundation struct TemperatureValue { let current: Double - let max: Double let min: Double + let max: Double } diff --git a/SwiftyForecast/Network/NetworkResponse/ResponseParser.swift b/SwiftyForecast/Network/NetworkResponse/ResponseParser.swift index 81a900f0..77be4de8 100644 --- a/SwiftyForecast/Network/NetworkResponse/ResponseParser.swift +++ b/SwiftyForecast/Network/NetworkResponse/ResponseParser.swift @@ -11,13 +11,16 @@ struct ResponseParser { let currentIcon = current.conditions.first?.icon ?? InvalidReference.undefined let dayNightSign = String(currentIcon.suffix(1)) let dayNightState = DayNightState(rawValue: dayNightSign) ?? .day + let temperatureValue = TemperatureValue( + current: current.main.temp, + min: current.main.tempMin, + max: current.main.tempMax + ) let currentWeatherModel = CurrentWeatherModel( date: currentDate, dayNightState: dayNightState, - temperature: current.main.temp, - maxTemperature: current.main.tempMax, - minTemperature: current.main.tempMin, + temperatureValue: temperatureValue, description: currentDescription, icon: currentIcon, humidity: current.main.humidity, diff --git a/SwiftyForecast/Screens/LocationSearch/LocationCurrentWeather/HourlyForecastChart/HourlyForecastChartDataSource.swift b/SwiftyForecast/Screens/LocationSearch/LocationCurrentWeather/HourlyForecastChart/HourlyForecastChartDataSource.swift index d7c7f399..a8623729 100644 --- a/SwiftyForecast/Screens/LocationSearch/LocationCurrentWeather/HourlyForecastChart/HourlyForecastChartDataSource.swift +++ b/SwiftyForecast/Screens/LocationSearch/LocationCurrentWeather/HourlyForecastChart/HourlyForecastChartDataSource.swift @@ -6,6 +6,7 @@ // Copyright © 2023 Pawel Milek. All rights reserved. // +// swiftlint:disable identifier_name import Foundation struct HourlyForecastChartDataSource: Identifiable, Equatable { @@ -15,25 +16,16 @@ struct HourlyForecastChartDataSource: Identifiable, Equatable { let temperatureFormatted: String let iconURL: URL? - private let temperatureFormatterFactory: TemperatureFormatterFactoryProtocol - private let notationController: NotationController - init( model: HourlyForecastModel, - temperatureFormatterFactory: TemperatureFormatterFactoryProtocol = TemperatureFormatterFactory(), - notationController: NotationController = NotationController() + temperatureRenderer: TemperatureRenderer = TemperatureRenderer() ) { - self.temperatureFormatterFactory = temperatureFormatterFactory - self.notationController = notationController hour = model.date.formatted(date: .omitted, time: .shortened) - let formatter = temperatureFormatterFactory.make( - by: notationController.temperatureNotation, - valueInKelvin: model.temperature - ) + let rendered = temperatureRenderer.render(model.temperature) - temperatureValue = formatter.currentValue - temperatureFormatted = formatter.currentFormatted + temperatureValue = rendered.currentValue + temperatureFormatted = rendered.current iconURL = WeatherEndpoint.icon(symbol: model.icon).url } diff --git a/SwiftyForecast/Screens/LocationSearch/LocationCurrentWeather/LocationWeatherView+ViewModel.swift b/SwiftyForecast/Screens/LocationSearch/LocationCurrentWeather/LocationWeatherView+ViewModel.swift index ea450887..69f7e43f 100644 --- a/SwiftyForecast/Screens/LocationSearch/LocationCurrentWeather/LocationWeatherView+ViewModel.swift +++ b/SwiftyForecast/Screens/LocationSearch/LocationCurrentWeather/LocationWeatherView+ViewModel.swift @@ -26,21 +26,15 @@ extension LocationWeatherView { private let searchCompletion: MKLocalSearchCompletion private let service: WeatherServiceProtocol - private let temperatureVolumeFormatter: TemperatureFormatterFactoryProtocol - private let notationController: NotationController private let databaseManager: DatabaseManager private let appStoreReviewCenter: ReviewNotificationCenter init(searchCompletion: MKLocalSearchCompletion, service: WeatherServiceProtocol = WeatherService(), - temperatureVolumeFactory: TemperatureFormatterFactoryProtocol = TemperatureFormatterFactory(), - notationController: NotationController = NotationController(), databaseManager: DatabaseManager = RealmManager.shared, appStoreReviewCenter: ReviewNotificationCenter = ReviewNotificationCenter()) { self.searchCompletion = searchCompletion self.service = service - self.temperatureVolumeFormatter = temperatureVolumeFactory - self.notationController = notationController self.databaseManager = databaseManager self.appStoreReviewCenter = appStoreReviewCenter subscriteToPublishers() diff --git a/SwiftyForecast/Screens/Main/Daily/ViewModel/DailyViewCell+ViewModel.swift b/SwiftyForecast/Screens/Main/Daily/ViewModel/DailyViewCell+ViewModel.swift index 870ff1b4..84c4925e 100644 --- a/SwiftyForecast/Screens/Main/Daily/ViewModel/DailyViewCell+ViewModel.swift +++ b/SwiftyForecast/Screens/Main/Daily/ViewModel/DailyViewCell+ViewModel.swift @@ -8,29 +8,22 @@ extension DailyViewCell { private(set) var iconURL: URL? private let model: DailyForecastModel - private let temperatureVolumeFactory: TemperatureFormatterFactoryProtocol - private let notationController: NotationController + private let temperatureRenderer: TemperatureRenderer init( model: DailyForecastModel, - temperatureVolumeFactory: TemperatureFormatterFactoryProtocol = TemperatureFormatterFactory(), - notationController: NotationController = NotationController() + temperatureRenderer: TemperatureRenderer = TemperatureRenderer() ) { self.model = model - self.temperatureVolumeFactory = temperatureVolumeFactory - self.notationController = notationController + self.temperatureRenderer = temperatureRenderer attributedDate = DailyDateRenderer.render(model.date) iconURL = WeatherEndpoint.iconLarge(symbol: model.icon).url setTemperatureAccordingToUnitNotation(model.temperature) } private func setTemperatureAccordingToUnitNotation(_ valueInKelvin: Double) { - let temperatureVolume = temperatureVolumeFactory.make( - by: notationController.temperatureNotation, - valueInKelvin: valueInKelvin - ) - - temperature = temperatureVolume.currentFormatted + let rendered = temperatureRenderer.render(valueInKelvin) + temperature = rendered.current } func setTemperature() { diff --git a/SwiftyForecast/Screens/Main/Hourly/ViewModel/HourlyViewCell+ViewModel.swift b/SwiftyForecast/Screens/Main/Hourly/ViewModel/HourlyViewCell+ViewModel.swift index a4315553..7e313064 100644 --- a/SwiftyForecast/Screens/Main/Hourly/ViewModel/HourlyViewCell+ViewModel.swift +++ b/SwiftyForecast/Screens/Main/Hourly/ViewModel/HourlyViewCell+ViewModel.swift @@ -8,29 +8,22 @@ extension HourlyViewCell { private(set) var temperature = "" private let model: HourlyForecastModel - private let temperatureVolumeFactory: TemperatureFormatterFactoryProtocol - private let notationController: NotationController + private let temperatureRenderer: TemperatureRenderer init( model: HourlyForecastModel, - temperatureVolumeFactory: TemperatureFormatterFactoryProtocol = TemperatureFormatterFactory(), - notationController: NotationController = NotationController() + temperatureRenderer: TemperatureRenderer = TemperatureRenderer() ) { self.model = model - self.temperatureVolumeFactory = temperatureVolumeFactory - self.notationController = notationController + self.temperatureRenderer = temperatureRenderer time = model.date.shortTime iconURL = WeatherEndpoint.iconLarge(symbol: model.icon).url setTemperatureAccordingToUnitNotation(model.temperature) } private func setTemperatureAccordingToUnitNotation(_ valueInKelvin: Double) { - let temperatureVolume = temperatureVolumeFactory.make( - by: notationController.temperatureNotation, - valueInKelvin: valueInKelvin - ) - - temperature = temperatureVolume.currentFormatted + let rendered = temperatureRenderer.render(valueInKelvin) + temperature = rendered.current } func setTemperature() { diff --git a/SwiftyForecast/Screens/Main/ViewModel/WeatherViewController+ViewModel.swift b/SwiftyForecast/Screens/Main/ViewModel/WeatherViewController+ViewModel.swift index a1077fc9..beb97df6 100644 --- a/SwiftyForecast/Screens/Main/ViewModel/WeatherViewController+ViewModel.swift +++ b/SwiftyForecast/Screens/Main/ViewModel/WeatherViewController+ViewModel.swift @@ -19,23 +19,16 @@ extension WeatherViewController { private var cancellables = Set() private let service: WeatherServiceProtocol - private let temperatureFormatterFactory: TemperatureFormatterFactoryProtocol - private let notationController: NotationController private let measurementSystemNotification: MeasurementSystemNotification private let appStoreReviewCenter: ReviewNotificationCenter init( locationModel: LocationModel, service: WeatherServiceProtocol = WeatherService(), - temperatureFormatterFactory: TemperatureFormatterFactoryProtocol = TemperatureFormatterFactory(), - speedFormatterFactory: SpeedFormatterFactoryProtocol = SpeedFormatterFactory(), - notationController: NotationController = NotationController(), measurementSystemNotification: MeasurementSystemNotification = MeasurementSystemNotification(), appStoreReviewCenter: ReviewNotificationCenter = ReviewNotificationCenter() ) { self.service = service - self.temperatureFormatterFactory = temperatureFormatterFactory - self.notationController = notationController self.measurementSystemNotification = measurementSystemNotification self.appStoreReviewCenter = appStoreReviewCenter subscriteToPublishers() diff --git a/SwiftyForecast/Views/CurrentWeatherCard/CurrentWeatherCard+ViewModel.swift b/SwiftyForecast/Views/CurrentWeatherCard/CurrentWeatherCard+ViewModel.swift index fe3b010d..eefdbbd4 100644 --- a/SwiftyForecast/Views/CurrentWeatherCard/CurrentWeatherCard+ViewModel.swift +++ b/SwiftyForecast/Views/CurrentWeatherCard/CurrentWeatherCard+ViewModel.swift @@ -30,21 +30,19 @@ extension CurrentWeatherCard { private var cancellables = Set() private let service: WeatherServiceProtocol - private let temperatureFormatterFactory: TemperatureFormatterFactoryProtocol - private let speedFormatterFactory: SpeedFormatterFactoryProtocol - private let notationController: NotationController private let measurementSystemNotification: MeasurementSystemNotification + private let temperatureRenderer: TemperatureRenderer + private let speedRenderer: SpeedRenderer init(service: WeatherServiceProtocol = WeatherService(), - temperatureFormatterFactory: TemperatureFormatterFactoryProtocol = TemperatureFormatterFactory(), - speedFormatterFactory: SpeedFormatterFactoryProtocol = SpeedFormatterFactory(), - notationController: NotationController = NotationController(), + temperatureRenderer: TemperatureRenderer = TemperatureRenderer(), + speedRenderer: SpeedRenderer = SpeedRenderer(), measurementSystemNotification: MeasurementSystemNotification = MeasurementSystemNotification()) { self.service = service - self.temperatureFormatterFactory = temperatureFormatterFactory - self.speedFormatterFactory = speedFormatterFactory - self.notationController = notationController + self.temperatureRenderer = temperatureRenderer + self.speedRenderer = speedRenderer self.measurementSystemNotification = measurementSystemNotification + subscribeToPublisher() registerMeasurementSystemObserver() } @@ -67,27 +65,16 @@ extension CurrentWeatherCard { private func setTemperatureAccordingToUnitNotation() { guard let model else { return } - - let temperatureFormatter = temperatureFormatterFactory.make( - by: notationController.temperatureNotation, - valueInKelvin: TemperatureValue( - current: model.temperature, - max: model.maxTemperature, - min: model.minTemperature - ) - ) - temperature = temperatureFormatter.currentFormatted - temperatureMaxMin = "⏶ \(temperatureFormatter.maxFormatted) ⏷ \(temperatureFormatter.minFormatted)" + let rendered = temperatureRenderer.render(model.temperatureValue) + temperature = rendered.current + temperatureMaxMin = rendered.maxMin } private func setWindSpeedAccordingToMeasurementSystem() { guard let model else { return } - let speedFormatter = speedFormatterFactory.make( - by: notationController.measurementSystem, - valueInMetersPerSec: model.windSpeed - ) - windSpeed.value = speedFormatter.current + let rendered = speedRenderer.render(model.windSpeed) + windSpeed.value = rendered } private func registerMeasurementSystemObserver() {