@@ -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 {
|
@@ -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 |
} |
@@ -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) {
|
@@ -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 |
|