Skip to content

Commit cf76da1

Browse files
committed
Add current weather metrics to header
1 parent fc71a16 commit cf76da1

File tree

8 files changed

+126
-2
lines changed

8 files changed

+126
-2
lines changed

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,56 @@ In that model:
173173

174174
The current implementation does not include that settings page or persistence layer yet. It is a presentation-only improvement that makes the displayed unit match the phone setting without introducing additional app configuration UI.
175175

176+
## Current Weather Metrics
177+
178+
The forecast header now shows a compact current-conditions summary in addition to the icon and temperature. The added values are:
179+
180+
- wind speed in `km/h`
181+
- humidity as a percentage
182+
- precipitation as a percentage
183+
184+
These values are derived from the first forecast entry returned by OpenWeather, which is the same entry the app already uses as the source of truth for the current primary condition and current displayed temperature.
185+
186+
### OpenWeather Fields Used
187+
188+
The app now reads the following additional fields from the OpenWeather 5-day / 3-hour forecast response:
189+
190+
- `main.humidity`
191+
- `wind.speed`
192+
- `pop`
193+
194+
How they are presented in the UI:
195+
196+
- `main.humidity` -> displayed directly as `Humidity`
197+
- `wind.speed` -> converted from meters per second into kilometers per hour and displayed as `Wind`
198+
- `pop` -> converted into a percentage and displayed as `Precip`
199+
200+
### Important Note About Precipitation
201+
202+
The precipitation value currently shown in the app is **not** rainfall volume.
203+
204+
It is OpenWeather's `pop` field, which represents the **probability of precipitation** for that forecast entry. In practical terms:
205+
206+
- `20%` means a 20% chance of precipitation
207+
- it does **not** mean 20 mm of rain
208+
209+
If the app later needs actual precipitation amount, that would require additional handling for fields such as:
210+
211+
- `rain.3h`
212+
- `snow.3h`
213+
214+
Those are separate from precipitation probability and should be treated as different weather metrics in the UI.
215+
216+
### Current Limitation
217+
218+
The current metrics row is intentionally lightweight:
219+
220+
- it uses only the first/current forecast entry
221+
- it does not yet expose gusts, visibility, pressure, or actual rain/snow volume
222+
- precipitation is currently shown as chance-of-precipitation only
223+
224+
This is a good lightweight summary for the current header, but a production-grade weather app would likely expand this into a dedicated details surface or conditions panel.
225+
176226
## Configuration
177227

178228
The current testing configuration is defined in [AppConfiguration.swift](/Users/blessingmabunda/Documents/WeatherApp/WeatherApp/App/AppConfiguration.swift).
@@ -200,6 +250,7 @@ xcodebuild test -project WeatherApp.xcodeproj -scheme WeatherApp -destination 'p
200250
- Unit tests cover:
201251
- OpenWeather response mapping into app-owned models
202252
- OpenWeather condition-to-icon mapping for bundled weather assets
253+
- current weather metric mapping for humidity, wind speed, and precipitation probability
203254
- theme/background mapping
204255
- forecast view-model state transitions
205256
- presentation formatting helpers, including Celsius/Fahrenheit display conversion

WeatherApp/Domain/Models/WeatherSnapshot.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ struct WeatherSnapshot: Equatable {
44
let locationName: String
55
let coordinate: LocationCoordinate
66
let currentTemperatureCelsius: Int
7+
let primaryDescription: String
8+
let humidityPercentage: Int
9+
let windSpeedKilometersPerHour: Int
10+
let precipitationProbabilityPercentage: Int
711
let primaryCondition: WeatherConditionCategory
812
let primaryIcon: WeatherIconAsset
913
let forecastDays: [ForecastDay]

WeatherApp/Features/Forecast/Views/ForecastHeaderView.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ struct ForecastHeaderView: View {
1717
.font(.headline.weight(.semibold))
1818
.foregroundStyle(.white)
1919

20+
if snapshot.primaryDescription.isEmpty == false {
21+
Text(snapshot.primaryDescription)
22+
.font(.custom("Poppins-Medium", size: 14))
23+
.foregroundStyle(.white.opacity(0.9))
24+
}
25+
26+
HStack(spacing: 12) {
27+
metricLabel("Wind", value: "\(snapshot.windSpeedKilometersPerHour) km/h")
28+
metricLabel("Humidity", value: "\(snapshot.humidityPercentage)%")
29+
metricLabel("Precip", value: "\(snapshot.precipitationProbabilityPercentage)%")
30+
}
31+
2032
HStack(alignment: .center, spacing: 16) {
2133
headerIcon
2234

@@ -33,6 +45,18 @@ struct ForecastHeaderView: View {
3345
.frame(maxWidth: .infinity, alignment: .leading)
3446
}
3547

48+
private func metricLabel(_ title: String, value: String) -> some View {
49+
VStack(alignment: .leading, spacing: 2) {
50+
Text(title.uppercased())
51+
.font(.custom("Poppins-SemiBold", size: 10))
52+
.foregroundStyle(.white.opacity(0.72))
53+
54+
Text(value)
55+
.font(.custom("Poppins-Medium", size: 12))
56+
.foregroundStyle(.white)
57+
}
58+
}
59+
3660
@ViewBuilder
3761
private var headerIcon: some View {
3862
if UIImage(named: snapshot.primaryIcon.assetName) != nil {

WeatherApp/Features/Forecast/Views/ForecastScreen.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ private struct PreviewWeatherProvider: WeatherProviding {
107107
locationName: "Johannesburg",
108108
coordinate: coordinate,
109109
currentTemperatureCelsius: 23,
110+
primaryDescription: "Broken Clouds",
111+
humidityPercentage: 62,
112+
windSpeedKilometersPerHour: 18,
113+
precipitationProbabilityPercentage: 35,
110114
primaryCondition: .cloudy,
111115
primaryIcon: .cloud,
112116
forecastDays: [

WeatherApp/Services/Weather/OpenWeatherForecastMapper.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ struct OpenWeatherForecastMapper {
1111
date: Date(timeIntervalSince1970: entry.timestamp),
1212
temperature: Int(entry.main.temperature.rounded()),
1313
maxTemperature: Int(entry.main.maximumTemperature.rounded()),
14+
description: formattedDescription(from: primaryWeather),
15+
humidity: entry.main.humidity,
16+
windSpeedKilometersPerHour: Int(((entry.wind?.speed ?? 0) * 3.6).rounded()),
17+
precipitationProbabilityPercentage: Int(((entry.precipitationProbability ?? 0) * 100).rounded()),
1418
condition: WeatherConditionCategory(openWeatherCondition: primaryWeather?.main ?? ""),
1519
icon: iconMapper.icon(for: primaryWeather)
1620
)
@@ -23,6 +27,10 @@ struct OpenWeatherForecastMapper {
2327
locationName: response.city.name,
2428
coordinate: coordinate,
2529
currentTemperatureCelsius: currentEntry?.temperature ?? 0,
30+
primaryDescription: currentEntry?.description ?? "",
31+
humidityPercentage: currentEntry?.humidity ?? 0,
32+
windSpeedKilometersPerHour: currentEntry?.windSpeedKilometersPerHour ?? 0,
33+
precipitationProbabilityPercentage: currentEntry?.precipitationProbabilityPercentage ?? 0,
2634
primaryCondition: currentEntry?.condition ?? .cloudy,
2735
primaryIcon: currentEntry?.icon ?? .cloud,
2836
forecastDays: forecastDays
@@ -85,12 +93,21 @@ struct OpenWeatherForecastMapper {
8593
) -> MappedEntry? {
8694
entries.first { $0.condition == dominantCondition } ?? entries.first
8795
}
96+
97+
private func formattedDescription(from weather: OpenWeatherResponse.Weather?) -> String {
98+
let source = (weather?.description.isEmpty == false ? weather?.description : weather?.main) ?? ""
99+
return source.localizedCapitalized
100+
}
88101
}
89102

90103
private struct MappedEntry {
91104
let date: Date
92105
let temperature: Int
93106
let maxTemperature: Int
107+
let description: String
108+
let humidity: Int
109+
let windSpeedKilometersPerHour: Int
110+
let precipitationProbabilityPercentage: Int
94111
let condition: WeatherConditionCategory
95112
let icon: WeatherIconAsset
96113
}

WeatherApp/Services/Weather/OpenWeatherResponse.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,27 @@ struct OpenWeatherResponse: Decodable {
88
let timestamp: TimeInterval
99
let main: Main
1010
let weather: [Weather]
11+
let wind: Wind?
12+
let precipitationProbability: Double?
1113

1214
enum CodingKeys: String, CodingKey {
1315
case timestamp = "dt"
1416
case main
1517
case weather
18+
case wind
19+
case precipitationProbability = "pop"
1620
}
1721
}
1822

1923
struct Main: Decodable {
2024
let temperature: Double
2125
let maximumTemperature: Double
26+
let humidity: Int
2227

2328
enum CodingKeys: String, CodingKey {
2429
case temperature = "temp"
2530
case maximumTemperature = "temp_max"
31+
case humidity
2632
}
2733
}
2834

@@ -32,6 +38,10 @@ struct OpenWeatherResponse: Decodable {
3238
let description: String
3339
}
3440

41+
struct Wind: Decodable {
42+
let speed: Double
43+
}
44+
3545
struct City: Decodable {
3646
let name: String
3747
let timezone: Int

WeatherAppTests/ForecastScreenViewModelTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ final class ForecastScreenViewModelTests: XCTestCase {
88
locationName: "Cape Town",
99
coordinate: LocationCoordinate(latitude: -33.9249, longitude: 18.4241),
1010
currentTemperatureCelsius: 18,
11+
primaryDescription: "Clouds",
12+
humidityPercentage: 61,
13+
windSpeedKilometersPerHour: 12,
14+
precipitationProbabilityPercentage: 30,
1115
primaryCondition: .cloudy,
1216
primaryIcon: .cloud,
1317
forecastDays: [
@@ -65,6 +69,10 @@ final class ForecastScreenViewModelTests: XCTestCase {
6569
locationName: "Durban",
6670
coordinate: LocationCoordinate(latitude: -29.8587, longitude: 31.0218),
6771
currentTemperatureCelsius: 25,
72+
primaryDescription: "Clear Sky",
73+
humidityPercentage: 48,
74+
windSpeedKilometersPerHour: 16,
75+
precipitationProbabilityPercentage: 10,
6876
primaryCondition: .sunny,
6977
primaryIcon: .sun,
7078
forecastDays: [

WeatherAppTests/OpenWeatherForecastMapperTests.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ final class OpenWeatherForecastMapperTests: XCTestCase {
2121
)
2222

2323
XCTAssertEqual(snapshot.locationName, "Pretoria")
24+
XCTAssertEqual(snapshot.primaryDescription, "Clouds")
25+
XCTAssertEqual(snapshot.humidityPercentage, 56)
26+
XCTAssertEqual(snapshot.windSpeedKilometersPerHour, 14)
27+
XCTAssertEqual(snapshot.precipitationProbabilityPercentage, 20)
2428
XCTAssertEqual(snapshot.primaryCondition, .cloudy)
2529
XCTAssertEqual(snapshot.primaryIcon, .cloud)
2630
XCTAssertEqual(snapshot.forecastDays.count, 5)
@@ -32,8 +36,10 @@ final class OpenWeatherForecastMapperTests: XCTestCase {
3236
private func makeEntry(timestamp: TimeInterval, temp: Double, max: Double, code: Int, main: String) -> OpenWeatherResponse.ForecastEntry {
3337
OpenWeatherResponse.ForecastEntry(
3438
timestamp: timestamp,
35-
main: .init(temperature: temp, maximumTemperature: max),
36-
weather: [.init(id: code, main: main, description: main)]
39+
main: .init(temperature: temp, maximumTemperature: max, humidity: 56),
40+
weather: [.init(id: code, main: main, description: main)],
41+
wind: .init(speed: 4),
42+
precipitationProbability: 0.2
3743
)
3844
}
3945
}

0 commit comments

Comments
 (0)