Skip to content

Commit fc71a16

Browse files
committed
Add device-driven temperature unit display
1 parent 2cb542d commit fc71a16

File tree

6 files changed

+145
-6
lines changed

6 files changed

+145
-6
lines changed

README.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,49 @@ The project now separates:
130130

131131
This keeps the background logic stable and easy to reason about, while allowing the forecast rows and current-condition UI to become much more specific without changing the rest of the app's visual architecture.
132132

133+
## Temperature Unit Behavior
134+
135+
The app currently fetches temperatures from OpenWeather in **metric** units and keeps those values in Celsius inside the app's domain models.
136+
137+
Temperature conversion happens only at presentation time:
138+
139+
- if the phone's system temperature preference resolves to Celsius, the app displays `°C`
140+
- if the phone's system temperature preference resolves to Fahrenheit, the app converts the stored Celsius value and displays `°F`
141+
142+
This applies consistently to:
143+
144+
- the current temperature in the forecast header
145+
- each daily temperature shown in the forecast list
146+
147+
### Current Limitation
148+
149+
This is intentionally limited.
150+
151+
Right now the app does **not** offer an in-app temperature preference. It only follows the phone's current system preference for temperature units. That is acceptable for a lightweight prototype, but it is not ideal for a production weather app because users may want control that differs from their device-wide regional setting.
152+
153+
Examples:
154+
155+
- a user may want Fahrenheit in the weather app even if their phone is configured for a metric region
156+
- a user may want Celsius in the app while traveling in a locale that defaults to Fahrenheit
157+
158+
### Production Direction
159+
160+
For a production-ready version, the app should add a dedicated settings surface where the user can explicitly choose their preferred temperature unit.
161+
162+
That future settings flow should support:
163+
164+
- `Use System Setting`
165+
- `Celsius`
166+
- `Fahrenheit`
167+
168+
In that model:
169+
170+
- the app would still be able to respect the phone setting by default
171+
- the user would be able to override that default when needed
172+
- the chosen preference would be persisted and used consistently across the entire app
173+
174+
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.
175+
133176
## Configuration
134177

135178
The current testing configuration is defined in [AppConfiguration.swift](/Users/blessingmabunda/Documents/WeatherApp/WeatherApp/App/AppConfiguration.swift).
@@ -159,7 +202,7 @@ xcodebuild test -project WeatherApp.xcodeproj -scheme WeatherApp -destination 'p
159202
- OpenWeather condition-to-icon mapping for bundled weather assets
160203
- theme/background mapping
161204
- forecast view-model state transitions
162-
- presentation formatting helpers
205+
- presentation formatting helpers, including Celsius/Fahrenheit display conversion
163206
- Protocol-based test doubles isolate location, network, and weather flows.
164207
- UI verification is handled through SwiftUI previews plus view-model coverage rather than snapshot tooling.
165208

WeatherApp/Features/Forecast/Views/ForecastHeaderView.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ struct ForecastHeaderView: View {
2020
HStack(alignment: .center, spacing: 16) {
2121
headerIcon
2222

23-
Text("\(snapshot.currentTemperatureCelsius)°")
23+
Text(
24+
ForecastPresentationFormatter.temperatureString(
25+
celsius: snapshot.currentTemperatureCelsius
26+
)
27+
)
2428
.font(.system(size: 52, weight: .bold, design: .rounded))
2529
.foregroundStyle(.white)
2630
}

WeatherApp/Shared/ForecastPresentationFormatter.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ enum ForecastPresentationFormatter {
1313
return formatter.string(from: date)
1414
}
1515

16-
static func temperatureString(celsius: Int) -> String {
17-
"\(celsius)°"
16+
static func temperatureString(
17+
celsius: Int,
18+
unitPreference: TemperatureUnitPreference = TemperatureUnitPreference()
19+
) -> String {
20+
let displayValue = unitPreference.displayValue(forCelsius: celsius)
21+
return "\(displayValue)\(unitPreference.unitSuffix)"
1822
}
1923
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Foundation
2+
3+
enum TemperatureUnitPreference: Equatable {
4+
case celsius
5+
case fahrenheit
6+
7+
init(locale: Locale = .autoupdatingCurrent) {
8+
let preferredUnit = UnitTemperature(forLocale: locale)
9+
10+
switch preferredUnit {
11+
case UnitTemperature.fahrenheit:
12+
self = .fahrenheit
13+
default:
14+
self = .celsius
15+
}
16+
}
17+
18+
func displayValue(forCelsius celsius: Int) -> Int {
19+
switch self {
20+
case .celsius:
21+
return celsius
22+
case .fahrenheit:
23+
let measurement = Measurement(value: Double(celsius), unit: UnitTemperature.celsius)
24+
return Int(measurement.converted(to: .fahrenheit).value.rounded())
25+
}
26+
}
27+
28+
var unitSuffix: String {
29+
switch self {
30+
case .celsius:
31+
return "°C"
32+
case .fahrenheit:
33+
return "°F"
34+
}
35+
}
36+
}

WeatherAppTests/ForecastPresentationFormatterTests.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,34 @@ import XCTest
22
@testable import WeatherApp
33

44
final class ForecastPresentationFormatterTests: XCTestCase {
5-
func testTemperatureStringUsesDegreeSuffix() {
6-
XCTAssertEqual(ForecastPresentationFormatter.temperatureString(celsius: 24), "24°")
5+
func testTemperatureStringUsesCelsiusWhenPreferred() {
6+
XCTAssertEqual(
7+
ForecastPresentationFormatter.temperatureString(
8+
celsius: 24,
9+
unitPreference: .celsius
10+
),
11+
"24°C"
12+
)
13+
}
14+
15+
func testTemperatureStringConvertsToFahrenheitWhenPreferred() {
16+
XCTAssertEqual(
17+
ForecastPresentationFormatter.temperatureString(
18+
celsius: 24,
19+
unitPreference: .fahrenheit
20+
),
21+
"75°F"
22+
)
23+
}
24+
25+
func testTemperatureStringConvertsNegativeValuesToFahrenheit() {
26+
XCTAssertEqual(
27+
ForecastPresentationFormatter.temperatureString(
28+
celsius: -10,
29+
unitPreference: .fahrenheit
30+
),
31+
"14°F"
32+
)
733
}
834

935
func testWeekdayStringReturnsWeekdayName() {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import XCTest
2+
@testable import WeatherApp
3+
4+
final class TemperatureUnitPreferenceTests: XCTestCase {
5+
func testInitUsesFahrenheitLocalePreference() {
6+
XCTAssertEqual(
7+
TemperatureUnitPreference(locale: Locale(identifier: "en_US")),
8+
.fahrenheit
9+
)
10+
}
11+
12+
func testInitUsesCelsiusLocalePreference() {
13+
XCTAssertEqual(
14+
TemperatureUnitPreference(locale: Locale(identifier: "en_ZA")),
15+
.celsius
16+
)
17+
}
18+
19+
func testDisplayValueLeavesCelsiusUntouched() {
20+
XCTAssertEqual(TemperatureUnitPreference.celsius.displayValue(forCelsius: 18), 18)
21+
}
22+
23+
func testDisplayValueConvertsToRoundedFahrenheit() {
24+
XCTAssertEqual(TemperatureUnitPreference.fahrenheit.displayValue(forCelsius: 18), 64)
25+
}
26+
}

0 commit comments

Comments
 (0)