Newer Older
298 lines | 10.355kb
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
    private struct LoadResistanceSymbol: View {
20
        let color: Color
21

            
22
        var body: some View {
23
            GeometryReader { proxy in
24
                let width = proxy.size.width
25
                let height = proxy.size.height
26
                let midY = height / 2
27
                let startX = width * 0.10
28
                let endX = width * 0.90
29
                let boxMinX = width * 0.28
30
                let boxMaxX = width * 0.72
31
                let boxHeight = height * 0.34
32
                let boxRect = CGRect(
33
                    x: boxMinX,
34
                    y: midY - (boxHeight / 2),
35
                    width: boxMaxX - boxMinX,
36
                    height: boxHeight
37
                )
38
                let strokeWidth = max(1.2, height * 0.055)
39

            
40
                ZStack {
41
                    Path { path in
42
                        path.move(to: CGPoint(x: startX, y: midY))
43
                        path.addLine(to: CGPoint(x: boxRect.minX, y: midY))
44
                        path.move(to: CGPoint(x: boxRect.maxX, y: midY))
45
                        path.addLine(to: CGPoint(x: endX, y: midY))
46
                    }
47
                    .stroke(
48
                        color,
49
                        style: StrokeStyle(
50
                            lineWidth: strokeWidth,
51
                            lineCap: .round,
52
                            lineJoin: .round
53
                        )
54
                    )
55

            
56
                    Path { path in
57
                        path.addRect(boxRect)
58
                    }
59
                    .stroke(
60
                        color,
61
                        style: StrokeStyle(
62
                            lineWidth: strokeWidth,
63
                            lineCap: .round,
64
                            lineJoin: .round
65
                        )
66
                    )
67
                }
68
            }
69
            .padding(4)
70
        }
71
    }
Bogdan Timofte authored 2 weeks ago
72

            
73
    @EnvironmentObject private var meter: Meter
Bogdan Timofte authored 2 weeks ago
74
    var compactLayout: Bool = false
75
    var availableSize: CGSize? = nil
Bogdan Timofte authored 2 weeks ago
76

            
77
    var body: some View {
Bogdan Timofte authored 2 weeks ago
78
        VStack(alignment: .leading, spacing: 16) {
Bogdan Timofte authored 2 weeks ago
79
            HStack {
Bogdan Timofte authored 2 weeks ago
80
                Text("Live Data")
81
                    .font(.headline)
82
                Spacer()
83
                statusBadge
84
            }
85

            
Bogdan Timofte authored 2 weeks ago
86
            LazyVGrid(columns: liveMetricColumns, spacing: compactLayout ? 10 : 12) {
Bogdan Timofte authored 2 weeks ago
87
                liveMetricCard(
88
                    title: "Voltage",
89
                    symbol: "bolt.fill",
90
                    color: .green,
91
                    value: "\(meter.voltage.format(decimalDigits: 3)) V",
Bogdan Timofte authored 2 weeks ago
92
                    range: metricRange(
Bogdan Timofte authored 2 weeks ago
93
                        min: meter.measurements.voltage.context.minValue,
94
                        max: meter.measurements.voltage.context.maxValue,
95
                        unit: "V"
96
                    )
97
                )
98

            
99
                liveMetricCard(
100
                    title: "Current",
101
                    symbol: "waveform.path.ecg",
102
                    color: .blue,
103
                    value: "\(meter.current.format(decimalDigits: 3)) A",
Bogdan Timofte authored 2 weeks ago
104
                    range: metricRange(
Bogdan Timofte authored 2 weeks ago
105
                        min: meter.measurements.current.context.minValue,
106
                        max: meter.measurements.current.context.maxValue,
107
                        unit: "A"
108
                    )
109
                )
110

            
111
                liveMetricCard(
112
                    title: "Power",
113
                    symbol: "flame.fill",
114
                    color: .pink,
115
                    value: "\(meter.power.format(decimalDigits: 3)) W",
Bogdan Timofte authored 2 weeks ago
116
                    range: metricRange(
Bogdan Timofte authored 2 weeks ago
117
                        min: meter.measurements.power.context.minValue,
118
                        max: meter.measurements.power.context.maxValue,
119
                        unit: "W"
120
                    )
121
                )
122

            
123
                liveMetricCard(
124
                    title: "Temperature",
125
                    symbol: "thermometer.medium",
126
                    color: .orange,
127
                    value: meter.primaryTemperatureDescription,
Bogdan Timofte authored 2 weeks ago
128
                    range: temperatureRange()
Bogdan Timofte authored 2 weeks ago
129
                )
130

            
Bogdan Timofte authored 2 weeks ago
131
                liveMetricCard(
132
                    title: "Load",
133
                    customSymbol: AnyView(LoadResistanceSymbol(color: .yellow)),
134
                    color: .yellow,
135
                    value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
136
                    detailText: "Measured resistance"
137
                )
Bogdan Timofte authored 2 weeks ago
138

            
Bogdan Timofte authored 2 weeks ago
139
                liveMetricCard(
140
                    title: "RSSI",
141
                    symbol: "dot.radiowaves.left.and.right",
142
                    color: .mint,
143
                    value: "\(meter.btSerial.RSSI) dBm",
144
                    range: MetricRange(
145
                        minLabel: "Min",
146
                        maxLabel: "Max",
147
                        minValue: "\(meter.btSerial.minRSSI) dBm",
148
                        maxValue: "\(meter.btSerial.maxRSSI) dBm"
149
                    ),
150
                    valueFont: .system(compactLayout ? .subheadline : .headline, design: .rounded).weight(.bold)
151
                )
Bogdan Timofte authored 2 weeks ago
152
            }
153
        }
Bogdan Timofte authored 2 weeks ago
154
        .frame(maxWidth: .infinity, alignment: .topLeading)
Bogdan Timofte authored 2 weeks ago
155
    }
156

            
157
    private var liveMetricColumns: [GridItem] {
158
        if compactLayout {
Bogdan Timofte authored 2 weeks ago
159
            return Array(repeating: GridItem(.flexible(), spacing: 10), count: 3)
Bogdan Timofte authored 2 weeks ago
160
        }
161

            
162
        return [GridItem(.flexible()), GridItem(.flexible())]
Bogdan Timofte authored 2 weeks ago
163
    }
Bogdan Timofte authored 2 weeks ago
164

            
165
    private var statusBadge: some View {
166
        Text(meter.operationalState == .dataIsAvailable ? "Live" : "Waiting")
167
            .font(.caption.weight(.semibold))
168
            .padding(.horizontal, 10)
169
            .padding(.vertical, 6)
170
            .foregroundColor(meter.operationalState == .dataIsAvailable ? .green : .secondary)
171
            .meterCard(
172
                tint: meter.operationalState == .dataIsAvailable ? .green : .secondary,
173
                fillOpacity: 0.12,
174
                strokeOpacity: 0.16,
175
                cornerRadius: 999
176
            )
177
    }
178

            
Bogdan Timofte authored 2 weeks ago
179
    private var showsCompactMetricRange: Bool {
180
        compactLayout && (availableSize?.height ?? 0) >= 380
181
    }
182

            
183
    private var shouldShowMetricRange: Bool {
184
        !compactLayout || showsCompactMetricRange
185
    }
186

            
Bogdan Timofte authored 2 weeks ago
187
    private func liveMetricCard(
188
        title: String,
Bogdan Timofte authored 2 weeks ago
189
        symbol: String? = nil,
190
        customSymbol: AnyView? = nil,
Bogdan Timofte authored 2 weeks ago
191
        color: Color,
192
        value: String,
Bogdan Timofte authored 2 weeks ago
193
        range: MetricRange? = nil,
Bogdan Timofte authored 2 weeks ago
194
        detailText: String? = nil,
195
        valueFont: Font? = nil,
196
        valueLineLimit: Int = 1,
197
        valueMonospacedDigits: Bool = true,
198
        valueMinimumScaleFactor: CGFloat = 0.85
Bogdan Timofte authored 2 weeks ago
199
    ) -> some View {
200
        VStack(alignment: .leading, spacing: 10) {
Bogdan Timofte authored 2 weeks ago
201
            HStack(spacing: compactLayout ? 8 : 10) {
Bogdan Timofte authored 2 weeks ago
202
                Group {
203
                    if let customSymbol {
204
                        customSymbol
205
                    } else if let symbol {
206
                        Image(systemName: symbol)
207
                            .font(.system(size: compactLayout ? 14 : 15, weight: .semibold))
208
                            .foregroundColor(color)
209
                    }
210
                }
Bogdan Timofte authored 2 weeks ago
211
                    .frame(width: compactLayout ? 30 : 34, height: compactLayout ? 30 : 34)
Bogdan Timofte authored 2 weeks ago
212
                .background(Circle().fill(color.opacity(0.12)))
Bogdan Timofte authored 2 weeks ago
213

            
Bogdan Timofte authored 2 weeks ago
214
                Text(title)
215
                    .font((compactLayout ? Font.caption : .subheadline).weight(.semibold))
216
                    .foregroundColor(.secondary)
217
                    .lineLimit(1)
218

            
219
                Spacer(minLength: 0)
220
            }
Bogdan Timofte authored 2 weeks ago
221

            
Bogdan Timofte authored 2 weeks ago
222
            Group {
223
                if valueMonospacedDigits {
224
                    Text(value)
225
                        .monospacedDigit()
226
                } else {
227
                    Text(value)
228
                }
229
            }
230
            .font(valueFont ?? .system(compactLayout ? .headline : .title3, design: .rounded).weight(.bold))
231
            .lineLimit(valueLineLimit)
232
            .minimumScaleFactor(valueMinimumScaleFactor)
Bogdan Timofte authored 2 weeks ago
233

            
Bogdan Timofte authored 2 weeks ago
234
            if shouldShowMetricRange {
235
                if let range {
236
                    metricRangeTable(range)
237
                } else if let detailText, !detailText.isEmpty {
238
                    Text(detailText)
239
                        .font(.caption)
240
                        .foregroundColor(.secondary)
241
                        .lineLimit(2)
242
                }
Bogdan Timofte authored 2 weeks ago
243
            }
244
        }
Bogdan Timofte authored 2 weeks ago
245
        .frame(
246
            maxWidth: .infinity,
Bogdan Timofte authored 2 weeks ago
247
            minHeight: compactLayout ? (shouldShowMetricRange ? 132 : 96) : 128,
Bogdan Timofte authored 2 weeks ago
248
            alignment: .leading
249
        )
250
        .padding(compactLayout ? 12 : 16)
Bogdan Timofte authored 2 weeks ago
251
        .meterCard(tint: color, fillOpacity: 0.10, strokeOpacity: 0.12)
252
    }
253

            
Bogdan Timofte authored 2 weeks ago
254
    private func metricRangeTable(_ range: MetricRange) -> some View {
255
        VStack(alignment: .leading, spacing: 4) {
256
            HStack(spacing: 12) {
257
                Text(range.minLabel)
258
                Spacer(minLength: 0)
259
                Text(range.maxLabel)
260
            }
261
            .font(.caption2.weight(.semibold))
262
            .foregroundColor(.secondary)
263

            
264
            HStack(spacing: 12) {
265
                Text(range.minValue)
266
                    .monospacedDigit()
267
                Spacer(minLength: 0)
268
                Text(range.maxValue)
269
                    .monospacedDigit()
270
            }
271
            .font(.caption.weight(.medium))
272
            .foregroundColor(.primary)
273
        }
274
    }
275

            
276
    private func metricRange(min: Double, max: Double, unit: String) -> MetricRange? {
Bogdan Timofte authored 2 weeks ago
277
        guard min.isFinite, max.isFinite else { return nil }
Bogdan Timofte authored 2 weeks ago
278

            
279
        return MetricRange(
280
            minLabel: "Min",
281
            maxLabel: "Max",
282
            minValue: "\(min.format(decimalDigits: 3)) \(unit)",
283
            maxValue: "\(max.format(decimalDigits: 3)) \(unit)"
284
        )
285
    }
286

            
287
    private func temperatureRange() -> MetricRange? {
288
        let value = meter.primaryTemperatureDescription
289
        guard !value.isEmpty else { return nil }
290

            
291
        return MetricRange(
292
            minLabel: "Min",
293
            maxLabel: "Max",
294
            minValue: value,
295
            maxValue: value
296
        )
Bogdan Timofte authored 2 weeks ago
297
    }
Bogdan Timofte authored 2 weeks ago
298
}