Newer Older
314 lines | 10.916kb
Bogdan Timofte authored 2 weeks ago
1
//
2
//  LiveView.swift
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 09/03/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8

            
9
import SwiftUI
10

            
11
struct LiveView: View {
Bogdan Timofte authored 2 weeks ago
12
    private struct MetricRange {
13
        let minLabel: String
14
        let maxLabel: String
15
        let minValue: String
16
        let maxValue: String
17
    }
Bogdan Timofte authored 2 weeks ago
18

            
19
    @EnvironmentObject private var meter: Meter
Bogdan Timofte authored 2 weeks ago
20
    var compactLayout: Bool = false
21
    var availableSize: CGSize? = nil
Bogdan Timofte authored 2 weeks ago
22

            
23
    var body: some View {
Bogdan Timofte authored 2 weeks ago
24
        VStack(alignment: .leading, spacing: 16) {
Bogdan Timofte authored 2 weeks ago
25
            HStack {
Bogdan Timofte authored 2 weeks ago
26
                Text("Live Data")
27
                    .font(.headline)
28
                Spacer()
29
                statusBadge
30
            }
31

            
Bogdan Timofte authored 2 weeks ago
32
            LazyVGrid(columns: liveMetricColumns, spacing: compactLayout ? 10 : 12) {
Bogdan Timofte authored 2 weeks ago
33
                liveMetricCard(
34
                    title: "Voltage",
35
                    symbol: "bolt.fill",
36
                    color: .green,
37
                    value: "\(meter.voltage.format(decimalDigits: 3)) V",
Bogdan Timofte authored 2 weeks ago
38
                    range: metricRange(
Bogdan Timofte authored 2 weeks ago
39
                        min: meter.measurements.voltage.context.minValue,
40
                        max: meter.measurements.voltage.context.maxValue,
41
                        unit: "V"
42
                    )
43
                )
44

            
45
                liveMetricCard(
46
                    title: "Current",
47
                    symbol: "waveform.path.ecg",
48
                    color: .blue,
49
                    value: "\(meter.current.format(decimalDigits: 3)) A",
Bogdan Timofte authored 2 weeks ago
50
                    range: metricRange(
Bogdan Timofte authored 2 weeks ago
51
                        min: meter.measurements.current.context.minValue,
52
                        max: meter.measurements.current.context.maxValue,
53
                        unit: "A"
54
                    )
55
                )
56

            
57
                liveMetricCard(
58
                    title: "Power",
59
                    symbol: "flame.fill",
60
                    color: .pink,
61
                    value: "\(meter.power.format(decimalDigits: 3)) W",
Bogdan Timofte authored 2 weeks ago
62
                    range: metricRange(
Bogdan Timofte authored 2 weeks ago
63
                        min: meter.measurements.power.context.minValue,
64
                        max: meter.measurements.power.context.maxValue,
65
                        unit: "W"
66
                    )
67
                )
68

            
69
                liveMetricCard(
70
                    title: "Temperature",
71
                    symbol: "thermometer.medium",
72
                    color: .orange,
73
                    value: meter.primaryTemperatureDescription,
Bogdan Timofte authored 2 weeks ago
74
                    range: temperatureRange()
Bogdan Timofte authored 2 weeks ago
75
                )
76
            }
77

            
78
            if shouldShowSecondaryDetails {
Bogdan Timofte authored 2 weeks ago
79
                Group {
80
                    if compactLayout {
81
                        HStack(spacing: 12) {
82
                            if meter.loadResistance > 0 {
83
                                secondaryDetailChip(
84
                                    title: "Load",
85
                                    value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
86
                                    symbol: "cable.connector",
87
                                    color: .yellow
88
                                )
89
                            }
90

            
91
                            if shouldShowChargerType {
92
                                secondaryDetailChip(
93
                                    title: "Charger",
94
                                    value: meter.chargerTypeDescription,
95
                                    symbol: "bolt.badge.checkmark",
96
                                    color: .purple
97
                                )
98
                            }
99
                        }
100
                    } else {
101
                        VStack(alignment: .leading, spacing: 12) {
102
                            Text("Details")
103
                                .font(.subheadline.weight(.semibold))
104
                                .foregroundColor(.secondary)
Bogdan Timofte authored 2 weeks ago
105

            
Bogdan Timofte authored 2 weeks ago
106
                            if meter.loadResistance > 0 {
107
                                secondaryDetailRow(
108
                                    title: "Load",
109
                                    value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
110
                                    symbol: "cable.connector",
111
                                    color: .yellow
112
                                )
113
                            }
114

            
115
                            if shouldShowChargerType {
116
                                secondaryDetailRow(
117
                                    title: "Charger",
118
                                    value: meter.chargerTypeDescription,
119
                                    symbol: "bolt.badge.checkmark",
120
                                    color: .purple
121
                                )
122
                            }
123
                        }
Bogdan Timofte authored 2 weeks ago
124
                    }
Bogdan Timofte authored 2 weeks ago
125
                }
Bogdan Timofte authored 2 weeks ago
126
                .padding(compactLayout ? 14 : 18)
Bogdan Timofte authored 2 weeks ago
127
                .meterCard(tint: meter.color, fillOpacity: 0.06, strokeOpacity: 0.10)
Bogdan Timofte authored 2 weeks ago
128
            }
129
        }
Bogdan Timofte authored 2 weeks ago
130
        .frame(maxWidth: .infinity, alignment: .topLeading)
Bogdan Timofte authored 2 weeks ago
131
    }
132

            
133
    private var liveMetricColumns: [GridItem] {
134
        if compactLayout {
135
            let count = usesExpandedCompactLayout ? 2 : 4
136
            return Array(repeating: GridItem(.flexible(), spacing: 10), count: count)
137
        }
138

            
139
        return [GridItem(.flexible()), GridItem(.flexible())]
Bogdan Timofte authored 2 weeks ago
140
    }
Bogdan Timofte authored 2 weeks ago
141

            
142
    private var statusBadge: some View {
143
        Text(meter.operationalState == .dataIsAvailable ? "Live" : "Waiting")
144
            .font(.caption.weight(.semibold))
145
            .padding(.horizontal, 10)
146
            .padding(.vertical, 6)
147
            .foregroundColor(meter.operationalState == .dataIsAvailable ? .green : .secondary)
148
            .meterCard(
149
                tint: meter.operationalState == .dataIsAvailable ? .green : .secondary,
150
                fillOpacity: 0.12,
151
                strokeOpacity: 0.16,
152
                cornerRadius: 999
153
            )
154
    }
155

            
156
    private var shouldShowSecondaryDetails: Bool {
157
        meter.loadResistance > 0 || shouldShowChargerType
158
    }
159

            
160
    private var shouldShowChargerType: Bool {
161
        meter.supportsChargerDetection && meter.chargerTypeDescription != "Unknown"
162
    }
163

            
Bogdan Timofte authored 2 weeks ago
164
    private var usesExpandedCompactLayout: Bool {
165
        compactLayout && (availableSize?.height ?? 0) >= 520
166
    }
167

            
Bogdan Timofte authored 2 weeks ago
168
    private var showsCompactMetricRange: Bool {
169
        compactLayout && (availableSize?.height ?? 0) >= 380
170
    }
171

            
172
    private var shouldShowMetricRange: Bool {
173
        !compactLayout || showsCompactMetricRange
174
    }
175

            
Bogdan Timofte authored 2 weeks ago
176
    private func liveMetricCard(
177
        title: String,
178
        symbol: String,
179
        color: Color,
180
        value: String,
Bogdan Timofte authored 2 weeks ago
181
        range: MetricRange? = nil,
182
        detailText: String? = nil
Bogdan Timofte authored 2 weeks ago
183
    ) -> some View {
184
        VStack(alignment: .leading, spacing: 10) {
Bogdan Timofte authored 2 weeks ago
185
            HStack(spacing: compactLayout ? 8 : 10) {
Bogdan Timofte authored 2 weeks ago
186
                Image(systemName: symbol)
Bogdan Timofte authored 2 weeks ago
187
                    .font(.system(size: compactLayout ? 14 : 15, weight: .semibold))
Bogdan Timofte authored 2 weeks ago
188
                    .foregroundColor(color)
Bogdan Timofte authored 2 weeks ago
189
                    .frame(width: compactLayout ? 30 : 34, height: compactLayout ? 30 : 34)
Bogdan Timofte authored 2 weeks ago
190
                    .background(Circle().fill(color.opacity(0.12)))
191

            
Bogdan Timofte authored 2 weeks ago
192
                Text(title)
193
                    .font((compactLayout ? Font.caption : .subheadline).weight(.semibold))
194
                    .foregroundColor(.secondary)
195
                    .lineLimit(1)
196

            
197
                Spacer(minLength: 0)
198
            }
Bogdan Timofte authored 2 weeks ago
199

            
200
            Text(value)
Bogdan Timofte authored 2 weeks ago
201
                .font(.system(compactLayout ? .headline : .title3, design: .rounded).weight(.bold))
Bogdan Timofte authored 2 weeks ago
202
                .monospacedDigit()
203

            
Bogdan Timofte authored 2 weeks ago
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
                }
Bogdan Timofte authored 2 weeks ago
213
            }
214
        }
Bogdan Timofte authored 2 weeks ago
215
        .frame(
216
            maxWidth: .infinity,
Bogdan Timofte authored 2 weeks ago
217
            minHeight: compactLayout ? (shouldShowMetricRange ? 132 : 96) : 128,
Bogdan Timofte authored 2 weeks ago
218
            alignment: .leading
219
        )
220
        .padding(compactLayout ? 12 : 16)
Bogdan Timofte authored 2 weeks ago
221
        .meterCard(tint: color, fillOpacity: 0.10, strokeOpacity: 0.12)
222
    }
223

            
224
    private func secondaryDetailRow(
225
        title: String,
226
        value: String,
227
        symbol: String,
228
        color: Color
229
    ) -> some View {
230
        HStack(spacing: 12) {
231
            Image(systemName: symbol)
232
                .foregroundColor(color)
233
                .frame(width: 28)
234
            Text(title)
235
                .foregroundColor(.secondary)
236
            Spacer()
237
            Text(value)
238
                .fontWeight(.semibold)
239
                .multilineTextAlignment(.trailing)
240
        }
241
        .font(.footnote)
242
    }
243

            
Bogdan Timofte authored 2 weeks ago
244
    private func secondaryDetailChip(
245
        title: String,
246
        value: String,
247
        symbol: String,
248
        color: Color
249
    ) -> some View {
250
        HStack(spacing: 10) {
251
            Image(systemName: symbol)
252
                .foregroundColor(color)
253
                .frame(width: 22, height: 22)
254
                .background(Circle().fill(color.opacity(0.12)))
255

            
256
            VStack(alignment: .leading, spacing: 2) {
257
                Text(title)
258
                    .foregroundColor(.secondary)
259
                Text(value)
260
                    .fontWeight(.semibold)
261
                    .lineLimit(1)
262
            }
263

            
264
            Spacer(minLength: 0)
265
        }
266
        .font(.caption)
267
        .frame(maxWidth: .infinity, alignment: .leading)
268
    }
269

            
Bogdan Timofte authored 2 weeks ago
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? {
Bogdan Timofte authored 2 weeks ago
293
        guard min.isFinite, max.isFinite else { return nil }
Bogdan Timofte authored 2 weeks ago
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
        )
Bogdan Timofte authored 2 weeks ago
313
    }
Bogdan Timofte authored 2 weeks ago
314
}