Newer Older
237 lines | 8.338kb
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 {
12
    @EnvironmentObject private var meter: Meter
Bogdan Timofte authored 2 weeks ago
13
    var compactLayout: Bool = false
14
    var availableSize: CGSize? = nil
Bogdan Timofte authored 2 weeks ago
15

            
16
    var body: some View {
Bogdan Timofte authored 2 weeks ago
17
        VStack(alignment: .leading, spacing: 16) {
Bogdan Timofte authored 2 weeks ago
18
            HStack {
Bogdan Timofte authored 2 weeks ago
19
                Text("Live Data")
20
                    .font(.headline)
21
                Spacer()
22
                statusBadge
23
            }
24

            
Bogdan Timofte authored 2 weeks ago
25
            LazyVGrid(columns: liveMetricColumns, spacing: compactLayout ? 10 : 12) {
Bogdan Timofte authored 2 weeks ago
26
                liveMetricCard(
27
                    title: "Voltage",
28
                    symbol: "bolt.fill",
29
                    color: .green,
30
                    value: "\(meter.voltage.format(decimalDigits: 3)) V",
Bogdan Timofte authored 2 weeks ago
31
                    range: metricRange(
Bogdan Timofte authored 2 weeks ago
32
                        min: meter.measurements.voltage.context.minValue,
33
                        max: meter.measurements.voltage.context.maxValue,
34
                        unit: "V"
35
                    )
36
                )
37

            
38
                liveMetricCard(
39
                    title: "Current",
40
                    symbol: "waveform.path.ecg",
41
                    color: .blue,
42
                    value: "\(meter.current.format(decimalDigits: 3)) A",
Bogdan Timofte authored 2 weeks ago
43
                    range: metricRange(
Bogdan Timofte authored 2 weeks ago
44
                        min: meter.measurements.current.context.minValue,
45
                        max: meter.measurements.current.context.maxValue,
46
                        unit: "A"
47
                    )
48
                )
49

            
50
                liveMetricCard(
51
                    title: "Power",
52
                    symbol: "flame.fill",
53
                    color: .pink,
54
                    value: "\(meter.power.format(decimalDigits: 3)) W",
Bogdan Timofte authored 2 weeks ago
55
                    range: metricRange(
Bogdan Timofte authored 2 weeks ago
56
                        min: meter.measurements.power.context.minValue,
57
                        max: meter.measurements.power.context.maxValue,
58
                        unit: "W"
59
                    )
60
                )
61

            
62
                liveMetricCard(
63
                    title: "Temperature",
64
                    symbol: "thermometer.medium",
65
                    color: .orange,
66
                    value: meter.primaryTemperatureDescription,
Bogdan Timofte authored 2 weeks ago
67
                    range: temperatureRange()
Bogdan Timofte authored 2 weeks ago
68
                )
69

            
Bogdan Timofte authored 2 weeks ago
70
                liveMetricCard(
71
                    title: "Load",
Bogdan Timofte authored a week ago
72
                    customSymbol: AnyView(LoadResistanceSymbolView(color: .yellow)),
Bogdan Timofte authored 2 weeks ago
73
                    color: .yellow,
74
                    value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
75
                    detailText: "Measured resistance"
76
                )
Bogdan Timofte authored 2 weeks ago
77

            
Bogdan Timofte authored 2 weeks ago
78
                liveMetricCard(
79
                    title: "RSSI",
80
                    symbol: "dot.radiowaves.left.and.right",
81
                    color: .mint,
Bogdan Timofte authored 2 weeks ago
82
                    value: "\(meter.btSerial.averageRSSI) dBm",
Bogdan Timofte authored a week ago
83
                    range: LiveMetricRange(
Bogdan Timofte authored 2 weeks ago
84
                        minLabel: "Min",
85
                        maxLabel: "Max",
86
                        minValue: "\(meter.btSerial.minRSSI) dBm",
87
                        maxValue: "\(meter.btSerial.maxRSSI) dBm"
88
                    ),
89
                    valueFont: .system(compactLayout ? .subheadline : .headline, design: .rounded).weight(.bold)
90
                )
Bogdan Timofte authored 2 weeks ago
91
            }
92
        }
Bogdan Timofte authored 2 weeks ago
93
        .frame(maxWidth: .infinity, alignment: .topLeading)
Bogdan Timofte authored 2 weeks ago
94
    }
95

            
96
    private var liveMetricColumns: [GridItem] {
97
        if compactLayout {
Bogdan Timofte authored 2 weeks ago
98
            return Array(repeating: GridItem(.flexible(), spacing: 10), count: 3)
Bogdan Timofte authored 2 weeks ago
99
        }
100

            
101
        return [GridItem(.flexible()), GridItem(.flexible())]
Bogdan Timofte authored 2 weeks ago
102
    }
Bogdan Timofte authored 2 weeks ago
103

            
104
    private var statusBadge: some View {
105
        Text(meter.operationalState == .dataIsAvailable ? "Live" : "Waiting")
106
            .font(.caption.weight(.semibold))
107
            .padding(.horizontal, 10)
108
            .padding(.vertical, 6)
109
            .foregroundColor(meter.operationalState == .dataIsAvailable ? .green : .secondary)
110
            .meterCard(
111
                tint: meter.operationalState == .dataIsAvailable ? .green : .secondary,
112
                fillOpacity: 0.12,
113
                strokeOpacity: 0.16,
114
                cornerRadius: 999
115
            )
116
    }
117

            
Bogdan Timofte authored 2 weeks ago
118
    private var showsCompactMetricRange: Bool {
119
        compactLayout && (availableSize?.height ?? 0) >= 380
120
    }
121

            
122
    private var shouldShowMetricRange: Bool {
123
        !compactLayout || showsCompactMetricRange
124
    }
125

            
Bogdan Timofte authored 2 weeks ago
126
    private func liveMetricCard(
127
        title: String,
Bogdan Timofte authored 2 weeks ago
128
        symbol: String? = nil,
129
        customSymbol: AnyView? = nil,
Bogdan Timofte authored 2 weeks ago
130
        color: Color,
131
        value: String,
Bogdan Timofte authored a week ago
132
        range: LiveMetricRange? = nil,
Bogdan Timofte authored 2 weeks ago
133
        detailText: String? = nil,
134
        valueFont: Font? = nil,
135
        valueLineLimit: Int = 1,
136
        valueMonospacedDigits: Bool = true,
137
        valueMinimumScaleFactor: CGFloat = 0.85
Bogdan Timofte authored 2 weeks ago
138
    ) -> some View {
139
        VStack(alignment: .leading, spacing: 10) {
Bogdan Timofte authored 2 weeks ago
140
            HStack(spacing: compactLayout ? 8 : 10) {
Bogdan Timofte authored 2 weeks ago
141
                Group {
142
                    if let customSymbol {
143
                        customSymbol
144
                    } else if let symbol {
145
                        Image(systemName: symbol)
146
                            .font(.system(size: compactLayout ? 14 : 15, weight: .semibold))
147
                            .foregroundColor(color)
148
                    }
149
                }
Bogdan Timofte authored 2 weeks ago
150
                    .frame(width: compactLayout ? 30 : 34, height: compactLayout ? 30 : 34)
Bogdan Timofte authored 2 weeks ago
151
                .background(Circle().fill(color.opacity(0.12)))
Bogdan Timofte authored 2 weeks ago
152

            
Bogdan Timofte authored 2 weeks ago
153
                Text(title)
154
                    .font((compactLayout ? Font.caption : .subheadline).weight(.semibold))
155
                    .foregroundColor(.secondary)
156
                    .lineLimit(1)
157

            
158
                Spacer(minLength: 0)
159
            }
Bogdan Timofte authored 2 weeks ago
160

            
Bogdan Timofte authored 2 weeks ago
161
            Group {
162
                if valueMonospacedDigits {
163
                    Text(value)
164
                        .monospacedDigit()
165
                } else {
166
                    Text(value)
167
                }
168
            }
169
            .font(valueFont ?? .system(compactLayout ? .headline : .title3, design: .rounded).weight(.bold))
170
            .lineLimit(valueLineLimit)
171
            .minimumScaleFactor(valueMinimumScaleFactor)
Bogdan Timofte authored 2 weeks ago
172

            
Bogdan Timofte authored 2 weeks ago
173
            if shouldShowMetricRange {
174
                if let range {
175
                    metricRangeTable(range)
176
                } else if let detailText, !detailText.isEmpty {
177
                    Text(detailText)
178
                        .font(.caption)
179
                        .foregroundColor(.secondary)
180
                        .lineLimit(2)
181
                }
Bogdan Timofte authored 2 weeks ago
182
            }
183
        }
Bogdan Timofte authored 2 weeks ago
184
        .frame(
185
            maxWidth: .infinity,
Bogdan Timofte authored 2 weeks ago
186
            minHeight: compactLayout ? (shouldShowMetricRange ? 132 : 96) : 128,
Bogdan Timofte authored 2 weeks ago
187
            alignment: .leading
188
        )
189
        .padding(compactLayout ? 12 : 16)
Bogdan Timofte authored 2 weeks ago
190
        .meterCard(tint: color, fillOpacity: 0.10, strokeOpacity: 0.12)
191
    }
192

            
Bogdan Timofte authored a week ago
193
    private func metricRangeTable(_ range: LiveMetricRange) -> some View {
Bogdan Timofte authored 2 weeks ago
194
        VStack(alignment: .leading, spacing: 4) {
195
            HStack(spacing: 12) {
196
                Text(range.minLabel)
197
                Spacer(minLength: 0)
198
                Text(range.maxLabel)
199
            }
200
            .font(.caption2.weight(.semibold))
201
            .foregroundColor(.secondary)
202

            
203
            HStack(spacing: 12) {
204
                Text(range.minValue)
205
                    .monospacedDigit()
206
                Spacer(minLength: 0)
207
                Text(range.maxValue)
208
                    .monospacedDigit()
209
            }
210
            .font(.caption.weight(.medium))
211
            .foregroundColor(.primary)
212
        }
213
    }
214

            
Bogdan Timofte authored a week ago
215
    private func metricRange(min: Double, max: Double, unit: String) -> LiveMetricRange? {
Bogdan Timofte authored 2 weeks ago
216
        guard min.isFinite, max.isFinite else { return nil }
Bogdan Timofte authored 2 weeks ago
217

            
Bogdan Timofte authored a week ago
218
        return LiveMetricRange(
Bogdan Timofte authored 2 weeks ago
219
            minLabel: "Min",
220
            maxLabel: "Max",
221
            minValue: "\(min.format(decimalDigits: 3)) \(unit)",
222
            maxValue: "\(max.format(decimalDigits: 3)) \(unit)"
223
        )
224
    }
225

            
Bogdan Timofte authored a week ago
226
    private func temperatureRange() -> LiveMetricRange? {
Bogdan Timofte authored 2 weeks ago
227
        let value = meter.primaryTemperatureDescription
228
        guard !value.isEmpty else { return nil }
229

            
Bogdan Timofte authored a week ago
230
        return LiveMetricRange(
Bogdan Timofte authored 2 weeks ago
231
            minLabel: "Min",
232
            maxLabel: "Max",
233
            minValue: value,
234
            maxValue: value
235
        )
Bogdan Timofte authored 2 weeks ago
236
    }
Bogdan Timofte authored 2 weeks ago
237
}