Showing 4 changed files with 157 additions and 39 deletions
+72 -10
USB Meter/Model/Meter.swift
@@ -53,6 +53,17 @@ enum TemperatureUnitPreference: String, CaseIterable, Identifiable {
53 53
     }
54 54
 }
55 55
 
56
+private extension TemperatureUnitPreference {
57
+    var localeTitle: String {
58
+        switch self {
59
+        case .celsius:
60
+            return "System (Celsius)"
61
+        case .fahrenheit:
62
+            return "System (Fahrenheit)"
63
+        }
64
+    }
65
+}
66
+
56 67
 enum ChargeRecordState {
57 68
     case waitingForStart
58 69
     case active
@@ -217,25 +228,76 @@ class Meter : NSObject, ObservableObject, Identifiable {
217 228
 
218 229
     var temperatureUnitDescription: String {
219 230
         if supportsManualTemperatureUnitSelection {
220
-            return tc66TemperatureUnitPreference.title
231
+            return "Device-defined"
221 232
         }
222
-        return supportsFahrenheit ? "Celsius / Fahrenheit" : "Celsius"
233
+        return systemTemperatureUnitPreference.localeTitle
223 234
     }
224 235
 
225 236
     var primaryTemperatureDescription: String {
226
-        let value = temperatureCelsius.format(decimalDigits: 0)
237
+        let value = displayedTemperatureValue.format(decimalDigits: 0)
227 238
         if supportsManualTemperatureUnitSelection {
228
-            return "\(value)\(tc66TemperatureUnitPreference.symbol)"
229
-        }
230
-        if let symbol = capabilities.primaryTemperatureUnitSymbol {
231
-            return "\(value)\(symbol)"
239
+            return "\(value)°"
232 240
         }
233
-        return value
241
+        return "\(value)\(systemTemperatureUnitPreference.symbol)"
234 242
     }
235 243
 
236 244
     var secondaryTemperatureDescription: String? {
237
-        guard supportsFahrenheit else { return nil }
238
-        return "\(temperatureFahrenheit.format(decimalDigits: 0))℉"
245
+        nil
246
+    }
247
+
248
+    var displayedTemperatureValue: Double {
249
+        if supportsManualTemperatureUnitSelection {
250
+            return temperatureCelsius
251
+        }
252
+        switch systemTemperatureUnitPreference {
253
+        case .celsius:
254
+            return displayedTemperatureCelsius
255
+        case .fahrenheit:
256
+            return displayedTemperatureFahrenheit
257
+        }
258
+    }
259
+
260
+    private var displayedTemperatureCelsius: Double {
261
+        if supportsManualTemperatureUnitSelection {
262
+            switch tc66TemperatureUnitPreference {
263
+            case .celsius:
264
+                return temperatureCelsius
265
+            case .fahrenheit:
266
+                return (temperatureCelsius - 32) * 5 / 9
267
+            }
268
+        }
269
+        return temperatureCelsius
270
+    }
271
+
272
+    private var displayedTemperatureFahrenheit: Double {
273
+        if supportsManualTemperatureUnitSelection {
274
+            switch tc66TemperatureUnitPreference {
275
+            case .celsius:
276
+                return (temperatureCelsius * 9 / 5) + 32
277
+            case .fahrenheit:
278
+                return temperatureCelsius
279
+            }
280
+        }
281
+        if supportsFahrenheit, temperatureFahrenheit.isFinite {
282
+            return temperatureFahrenheit
283
+        }
284
+        return (temperatureCelsius * 9 / 5) + 32
285
+    }
286
+
287
+    private var systemTemperatureUnitPreference: TemperatureUnitPreference {
288
+        let locale = Locale.autoupdatingCurrent
289
+        if #available(iOS 16.0, *) {
290
+            switch locale.measurementSystem {
291
+            case .us:
292
+                return .fahrenheit
293
+            default:
294
+                return .celsius
295
+            }
296
+        }
297
+
298
+        let regionCode = locale.regionCode ?? ""
299
+        let fahrenheitRegions: Set<String> = ["US", "BS", "BZ", "KY", "PW", "LR", "FM", "MH"]
300
+        return fahrenheitRegions.contains(regionCode) ? .fahrenheit : .celsius
239 301
     }
240 302
 
241 303
     var currentScreenDescription: String {
+81 -24
USB Meter/Views/Meter/LiveView.swift
@@ -9,6 +9,12 @@
9 9
 import SwiftUI
10 10
 
11 11
 struct LiveView: View {
12
+    private struct MetricRange {
13
+        let minLabel: String
14
+        let maxLabel: String
15
+        let minValue: String
16
+        let maxValue: String
17
+    }
12 18
     
13 19
     @EnvironmentObject private var meter: Meter
14 20
     var compactLayout: Bool = false
@@ -29,7 +35,7 @@ struct LiveView: View {
29 35
                     symbol: "bolt.fill",
30 36
                     color: .green,
31 37
                     value: "\(meter.voltage.format(decimalDigits: 3)) V",
32
-                    range: rangeText(
38
+                    range: metricRange(
33 39
                         min: meter.measurements.voltage.context.minValue,
34 40
                         max: meter.measurements.voltage.context.maxValue,
35 41
                         unit: "V"
@@ -41,7 +47,7 @@ struct LiveView: View {
41 47
                     symbol: "waveform.path.ecg",
42 48
                     color: .blue,
43 49
                     value: "\(meter.current.format(decimalDigits: 3)) A",
44
-                    range: rangeText(
50
+                    range: metricRange(
45 51
                         min: meter.measurements.current.context.minValue,
46 52
                         max: meter.measurements.current.context.maxValue,
47 53
                         unit: "A"
@@ -53,7 +59,7 @@ struct LiveView: View {
53 59
                     symbol: "flame.fill",
54 60
                     color: .pink,
55 61
                     value: "\(meter.power.format(decimalDigits: 3)) W",
56
-                    range: rangeText(
62
+                    range: metricRange(
57 63
                         min: meter.measurements.power.context.minValue,
58 64
                         max: meter.measurements.power.context.maxValue,
59 65
                         unit: "W"
@@ -65,14 +71,10 @@ struct LiveView: View {
65 71
                     symbol: "thermometer.medium",
66 72
                     color: .orange,
67 73
                     value: meter.primaryTemperatureDescription,
68
-                    range: meter.secondaryTemperatureDescription
74
+                    range: temperatureRange()
69 75
                 )
70 76
             }
71 77
 
72
-            if compactLayout && usesExpandedCompactLayout {
73
-                Spacer(minLength: 0)
74
-            }
75
-
76 78
             if shouldShowSecondaryDetails {
77 79
                 Group {
78 80
                     if compactLayout {
@@ -125,7 +127,7 @@ struct LiveView: View {
125 127
                 .meterCard(tint: meter.color, fillOpacity: 0.06, strokeOpacity: 0.10)
126 128
             }
127 129
         }
128
-        .frame(maxWidth: .infinity, maxHeight: compactLayout ? .infinity : nil, alignment: .topLeading)
130
+        .frame(maxWidth: .infinity, alignment: .topLeading)
129 131
     }
130 132
 
131 133
     private var liveMetricColumns: [GridItem] {
@@ -163,41 +165,56 @@ struct LiveView: View {
163 165
         compactLayout && (availableSize?.height ?? 0) >= 520
164 166
     }
165 167
 
168
+    private var showsCompactMetricRange: Bool {
169
+        compactLayout && (availableSize?.height ?? 0) >= 380
170
+    }
171
+
172
+    private var shouldShowMetricRange: Bool {
173
+        !compactLayout || showsCompactMetricRange
174
+    }
175
+
166 176
     private func liveMetricCard(
167 177
         title: String,
168 178
         symbol: String,
169 179
         color: Color,
170 180
         value: String,
171
-        range: String?
181
+        range: MetricRange? = nil,
182
+        detailText: String? = nil
172 183
     ) -> some View {
173 184
         VStack(alignment: .leading, spacing: 10) {
174
-            HStack {
185
+            HStack(spacing: compactLayout ? 8 : 10) {
175 186
                 Image(systemName: symbol)
176 187
                     .font(.system(size: compactLayout ? 14 : 15, weight: .semibold))
177 188
                     .foregroundColor(color)
178 189
                     .frame(width: compactLayout ? 30 : 34, height: compactLayout ? 30 : 34)
179 190
                     .background(Circle().fill(color.opacity(0.12)))
180
-                Spacer()
181
-            }
182 191
 
183
-            Text(title)
184
-                .font((compactLayout ? Font.caption : .subheadline).weight(.semibold))
185
-                .foregroundColor(.secondary)
192
+                Text(title)
193
+                    .font((compactLayout ? Font.caption : .subheadline).weight(.semibold))
194
+                    .foregroundColor(.secondary)
195
+                    .lineLimit(1)
196
+
197
+                Spacer(minLength: 0)
198
+            }
186 199
 
187 200
             Text(value)
188 201
                 .font(.system(compactLayout ? .headline : .title3, design: .rounded).weight(.bold))
189 202
                 .monospacedDigit()
190 203
 
191
-            if !compactLayout, let range, !range.isEmpty {
192
-                Text(range)
193
-                    .font(.caption)
194
-                    .foregroundColor(.secondary)
195
-                    .lineLimit(2)
204
+            if shouldShowMetricRange {
205
+                if let range {
206
+                    metricRangeTable(range)
207
+                } else if let detailText, !detailText.isEmpty {
208
+                    Text(detailText)
209
+                        .font(.caption)
210
+                        .foregroundColor(.secondary)
211
+                        .lineLimit(2)
212
+                }
196 213
             }
197 214
         }
198 215
         .frame(
199 216
             maxWidth: .infinity,
200
-            minHeight: compactLayout ? (usesExpandedCompactLayout ? 128 : 96) : 128,
217
+            minHeight: compactLayout ? (shouldShowMetricRange ? 132 : 96) : 128,
201 218
             alignment: .leading
202 219
         )
203 220
         .padding(compactLayout ? 12 : 16)
@@ -250,8 +267,48 @@ struct LiveView: View {
250 267
         .frame(maxWidth: .infinity, alignment: .leading)
251 268
     }
252 269
 
253
-    private func rangeText(min: Double, max: Double, unit: String) -> String? {
270
+    private func metricRangeTable(_ range: MetricRange) -> some View {
271
+        VStack(alignment: .leading, spacing: 4) {
272
+            HStack(spacing: 12) {
273
+                Text(range.minLabel)
274
+                Spacer(minLength: 0)
275
+                Text(range.maxLabel)
276
+            }
277
+            .font(.caption2.weight(.semibold))
278
+            .foregroundColor(.secondary)
279
+
280
+            HStack(spacing: 12) {
281
+                Text(range.minValue)
282
+                    .monospacedDigit()
283
+                Spacer(minLength: 0)
284
+                Text(range.maxValue)
285
+                    .monospacedDigit()
286
+            }
287
+            .font(.caption.weight(.medium))
288
+            .foregroundColor(.primary)
289
+        }
290
+    }
291
+
292
+    private func metricRange(min: Double, max: Double, unit: String) -> MetricRange? {
254 293
         guard min.isFinite, max.isFinite else { return nil }
255
-        return "Min \(min.format(decimalDigits: 3)) \(unit)  Max \(max.format(decimalDigits: 3)) \(unit)"
294
+
295
+        return MetricRange(
296
+            minLabel: "Min",
297
+            maxLabel: "Max",
298
+            minValue: "\(min.format(decimalDigits: 3)) \(unit)",
299
+            maxValue: "\(max.format(decimalDigits: 3)) \(unit)"
300
+        )
301
+    }
302
+
303
+    private func temperatureRange() -> MetricRange? {
304
+        let value = meter.primaryTemperatureDescription
305
+        guard !value.isEmpty else { return nil }
306
+
307
+        return MetricRange(
308
+            minLabel: "Min",
309
+            maxLabel: "Max",
310
+            minValue: value,
311
+            maxValue: value
312
+        )
256 313
     }
257 314
 }
+2 -2
USB Meter/Views/Meter/MeterSettingsView.swift
@@ -34,8 +34,8 @@ struct MeterSettingsView: View {
34 34
                 }
35 35
 
36 36
                 if meter.operationalState == .dataIsAvailable && meter.supportsManualTemperatureUnitSelection {
37
-                    settingsCard(title: "Temperature Unit", tint: .orange) {
38
-                        Text("TC66 reports temperature using the unit selected on the device. Keep this setting matched to the meter.")
37
+                    settingsCard(title: "Meter Temperature Unit", tint: .orange) {
38
+                        Text("TC66 temperature is shown as degrees without assuming Celsius or Fahrenheit. Keep this matched to the unit configured on the device so you can interpret the reading correctly.")
39 39
                             .font(.footnote)
40 40
                             .foregroundColor(.secondary)
41 41
                         Picker("", selection: $meter.tc66TemperatureUnitPreference) {
+2 -3
USB Meter/Views/Meter/MeterView.swift
@@ -108,7 +108,7 @@ struct MeterView: View {
108 108
                 landscapeFace {
109 109
                     LiveView(compactLayout: true, availableSize: size)
110 110
                         .padding(16)
111
-                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
111
+                        .frame(maxWidth: .infinity, alignment: .topLeading)
112 112
                         .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20)
113 113
                 }
114 114
 
@@ -138,8 +138,7 @@ struct MeterView: View {
138 138
         content()
139 139
             .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
140 140
         .padding(.horizontal, 12)
141
-        .padding(.top, 10)
142
-        .padding(.bottom, 20)
141
+        .padding(.vertical, 12)
143 142
         .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
144 143
     }
145 144