Newer Older
379 lines | 15.13kb
Bogdan Timofte authored 2 weeks ago
1
//
2
//  MeterView.swift
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 04/03/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8
// MARK: Parent frame https://stackoverflow.com/questions/56832865/how-to-access-parents-frame-in-swiftui
9

            
10
import SwiftUI
11
import CoreBluetooth
12

            
13
struct MeterView: View {
14

            
15
    @EnvironmentObject private var meter: Meter
16

            
17
    @State var dataGroupsViewVisibility: Bool = false
Bogdan Timofte authored 2 weeks ago
18
    @State var recordingViewVisibility: Bool = false
Bogdan Timofte authored 2 weeks ago
19
    @State var measurementsViewVisibility: Bool = false
20
    private var myBounds: CGRect { UIScreen.main.bounds }
Bogdan Timofte authored 2 weeks ago
21
    private let actionStripPadding: CGFloat = 10
22
    private let actionDividerWidth: CGFloat = 1
Bogdan Timofte authored 2 weeks ago
23
    private let actionButtonMaxWidth: CGFloat = 156
24
    private let actionButtonMinWidth: CGFloat = 88
25
    private let actionButtonHeight: CGFloat = 108
Bogdan Timofte authored 2 weeks ago
26

            
Bogdan Timofte authored 2 weeks ago
27
    var body: some View {
28
        ScrollView {
Bogdan Timofte authored 2 weeks ago
29
            VStack(alignment: .leading, spacing: 16) {
Bogdan Timofte authored 2 weeks ago
30
                connectionCard
Bogdan Timofte authored 2 weeks ago
31

            
Bogdan Timofte authored 2 weeks ago
32
                if meter.operationalState == .dataIsAvailable {
33
                    actionGrid
Bogdan Timofte authored 2 weeks ago
34

            
Bogdan Timofte authored 2 weeks ago
35
                    LiveView()
36
                        .padding(18)
37
                        .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20)
38

            
Bogdan Timofte authored 2 weeks ago
39
                    if meter.measurements.power.context.isValid {
40
                        MeasurementChartView()
41
                            .environmentObject(meter.measurements)
42
                            .frame(minHeight: myBounds.height / 3.4)
43
                            .padding(16)
44
                            .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20)
Bogdan Timofte authored 2 weeks ago
45
                    }
Bogdan Timofte authored 2 weeks ago
46

            
47
                    ControlView()
48
                        .padding(16)
49
                        .meterCard(tint: .indigo, fillOpacity: 0.12, strokeOpacity: 0.20)
Bogdan Timofte authored 2 weeks ago
50
                }
51
            }
Bogdan Timofte authored 2 weeks ago
52
            .padding()
Bogdan Timofte authored 2 weeks ago
53
        }
Bogdan Timofte authored 2 weeks ago
54
        .background(
55
            LinearGradient(
56
                colors: [
57
                    meter.color.opacity(0.22),
58
                    Color.secondary.opacity(0.08),
59
                    Color.clear
60
                ],
61
                startPoint: .topLeading,
62
                endPoint: .bottomTrailing
63
            )
64
            .ignoresSafeArea()
65
        )
Bogdan Timofte authored 2 weeks ago
66
        .navigationBarTitle("Meter")
Bogdan Timofte authored 2 weeks ago
67
        .navigationBarItems(trailing: HStack (spacing: 6) {
Bogdan Timofte authored 2 weeks ago
68
            if meter.operationalState > .notPresent {
69
                RSSIView(RSSI: meter.btSerial.RSSI)
Bogdan Timofte authored 2 weeks ago
70
                    .frame(width: 18, height: 18)
71
                    .padding(.leading, 6)
Bogdan Timofte authored 2 weeks ago
72
                    .padding(.vertical)
73
            }
Bogdan Timofte authored 2 weeks ago
74
            NavigationLink(destination: MeterInfoView().environmentObject(meter)) {
75
                Image(systemName: "info.circle.fill")
76
                    .padding(.vertical)
77
            }
Bogdan Timofte authored 2 weeks ago
78
            NavigationLink(destination: MeterSettingsView().environmentObject(meter)) {
Bogdan Timofte authored 2 weeks ago
79
                Image(systemName: "gearshape.fill")
Bogdan Timofte authored 2 weeks ago
80
                    .padding(.vertical)
81
            }
82
        })
83
    }
Bogdan Timofte authored 2 weeks ago
84

            
Bogdan Timofte authored 2 weeks ago
85
    private var connectionCard: some View {
86
        VStack(alignment: .leading, spacing: 18) {
Bogdan Timofte authored 2 weeks ago
87
            HStack(alignment: .top) {
88
                VStack(alignment: .leading, spacing: 6) {
89
                    Text(meter.name)
90
                        .font(.system(.title2, design: .rounded).weight(.bold))
91
                    Text(meter.deviceModelSummary)
92
                        .font(.subheadline.weight(.semibold))
93
                        .foregroundColor(.secondary)
94
                }
95
                Spacer()
96
                VStack(alignment: .trailing, spacing: 6) {
97
                    Text(statusText)
98
                        .font(.caption.weight(.bold))
99
                        .padding(.horizontal, 12)
100
                        .padding(.vertical, 6)
101
                        .meterCard(tint: statusColor, fillOpacity: 0.24, strokeOpacity: 0.30, cornerRadius: 999)
102
                    if meter.operationalState > .notPresent {
103
                        Text("RSSI \(meter.btSerial.RSSI)")
104
                            .font(.caption)
105
                            .foregroundColor(.secondary)
106
                    }
107
                }
108
            }
Bogdan Timofte authored 2 weeks ago
109

            
110
            connectionActionArea
Bogdan Timofte authored 2 weeks ago
111
        }
112
        .padding(20)
113
        .meterCard(tint: meter.color, fillOpacity: 0.22, strokeOpacity: 0.24)
114
    }
115

            
116
    private var actionGrid: some View {
Bogdan Timofte authored 2 weeks ago
117
        GeometryReader { proxy in
118
            let buttonWidth = actionButtonWidth(for: proxy.size.width)
Bogdan Timofte authored 2 weeks ago
119
            let stripWidth = actionStripWidth(for: buttonWidth)
Bogdan Timofte authored 2 weeks ago
120

            
121
            HStack {
122
                Spacer(minLength: 0)
Bogdan Timofte authored 2 weeks ago
123
                HStack(spacing: 0) {
Bogdan Timofte authored 2 weeks ago
124
                    meterSheetButton(icon: "square.grid.2x2.fill", title: meter.dataGroupsTitle, tint: .teal, width: buttonWidth) {
125
                        dataGroupsViewVisibility.toggle()
126
                    }
127
                    .sheet(isPresented: $dataGroupsViewVisibility) {
128
                        DataGroupsView(visibility: $dataGroupsViewVisibility)
129
                            .environmentObject(meter)
130
                    }
Bogdan Timofte authored 2 weeks ago
131

            
Bogdan Timofte authored 2 weeks ago
132
                    if meter.supportsRecordingView {
Bogdan Timofte authored 2 weeks ago
133
                        actionStripDivider
Bogdan Timofte authored 2 weeks ago
134
                        meterSheetButton(icon: "gauge.with.dots.needle.50percent", title: "Charge Record", tint: .pink, width: buttonWidth) {
135
                            recordingViewVisibility.toggle()
136
                        }
137
                        .sheet(isPresented: $recordingViewVisibility) {
138
                            RecordingView(visibility: $recordingViewVisibility)
139
                                .environmentObject(meter)
140
                        }
141
                    }
Bogdan Timofte authored 2 weeks ago
142

            
Bogdan Timofte authored 2 weeks ago
143
                    actionStripDivider
Bogdan Timofte authored 2 weeks ago
144
                    meterSheetButton(icon: "clock.arrow.circlepath", title: "App History", tint: .blue, width: buttonWidth) {
145
                        measurementsViewVisibility.toggle()
146
                    }
147
                    .sheet(isPresented: $measurementsViewVisibility) {
148
                        MeasurementsView(visibility: $measurementsViewVisibility)
149
                            .environmentObject(meter.measurements)
150
                    }
151
                }
Bogdan Timofte authored 2 weeks ago
152
                .padding(actionStripPadding)
153
                .frame(width: stripWidth)
154
                .meterCard(tint: Color.secondary, fillOpacity: 0.10, strokeOpacity: 0.16)
Bogdan Timofte authored 2 weeks ago
155
                Spacer(minLength: 0)
Bogdan Timofte authored 2 weeks ago
156
            }
157
        }
Bogdan Timofte authored 2 weeks ago
158
        .frame(height: actionButtonHeight + (actionStripPadding * 2))
Bogdan Timofte authored 2 weeks ago
159
    }
Bogdan Timofte authored 2 weeks ago
160

            
Bogdan Timofte authored 2 weeks ago
161
    private var connectionActionArea: some View {
Bogdan Timofte authored 2 weeks ago
162
        let connected = meter.operationalState >= .peripheralConnectionPending
Bogdan Timofte authored 2 weeks ago
163
        let tint = connected ? disconnectActionTint : connectActionTint
Bogdan Timofte authored 2 weeks ago
164

            
Bogdan Timofte authored 2 weeks ago
165
        return Group {
166
            if meter.operationalState == .notPresent {
Bogdan Timofte authored 2 weeks ago
167
                HStack(spacing: 10) {
168
                    Image(systemName: "exclamationmark.triangle.fill")
169
                        .foregroundColor(.orange)
170
                    Text("Not found at this time.")
171
                        .fontWeight(.semibold)
172
                    Spacer()
173
                }
174
                .padding(16)
Bogdan Timofte authored 2 weeks ago
175
                .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.18)
Bogdan Timofte authored 2 weeks ago
176
            } else {
Bogdan Timofte authored 2 weeks ago
177
                Button(action: {
Bogdan Timofte authored 2 weeks ago
178
                    if meter.operationalState < .peripheralConnectionPending {
Bogdan Timofte authored 2 weeks ago
179
                        meter.connect()
Bogdan Timofte authored 2 weeks ago
180
                    } else {
Bogdan Timofte authored 2 weeks ago
181
                        meter.disconnect()
182
                    }
183
                }) {
Bogdan Timofte authored 2 weeks ago
184
                    HStack(spacing: 12) {
Bogdan Timofte authored 2 weeks ago
185
                        Image(systemName: connected ? "xmark.circle.fill" : "bolt.horizontal.circle.fill")
Bogdan Timofte authored 2 weeks ago
186
                            .foregroundColor(tint)
187
                            .frame(width: 30, height: 30)
188
                            .background(Circle().fill(tint.opacity(0.12)))
Bogdan Timofte authored 2 weeks ago
189
                        Text(connected ? "Disconnect" : "Connect")
190
                            .fontWeight(.semibold)
Bogdan Timofte authored 2 weeks ago
191
                            .foregroundColor(.primary)
Bogdan Timofte authored 2 weeks ago
192
                        Spacer()
Bogdan Timofte authored 2 weeks ago
193
                    }
Bogdan Timofte authored 2 weeks ago
194
                    .padding(.horizontal, 18)
Bogdan Timofte authored 2 weeks ago
195
                    .padding(.vertical, 14)
Bogdan Timofte authored 2 weeks ago
196
                    .frame(maxWidth: .infinity)
Bogdan Timofte authored 2 weeks ago
197
                    .meterCard(tint: tint, fillOpacity: 0.14, strokeOpacity: 0.20)
Bogdan Timofte authored 2 weeks ago
198
                }
Bogdan Timofte authored 2 weeks ago
199
                .buttonStyle(.plain)
Bogdan Timofte authored 2 weeks ago
200
            }
201
        }
202
    }
Bogdan Timofte authored 2 weeks ago
203

            
Bogdan Timofte authored 2 weeks ago
204
    fileprivate func meterSheetButton(icon: String, title: String, tint: Color, width: CGFloat, action: @escaping () -> Void) -> some View {
Bogdan Timofte authored 2 weeks ago
205
        Button(action: action) {
Bogdan Timofte authored 2 weeks ago
206
            VStack(spacing: 10) {
Bogdan Timofte authored 2 weeks ago
207
                Image(systemName: icon)
Bogdan Timofte authored 2 weeks ago
208
                    .font(.system(size: 20, weight: .semibold))
209
                    .frame(width: 40, height: 40)
210
                    .background(Circle().fill(tint.opacity(0.14)))
Bogdan Timofte authored 2 weeks ago
211
                Text(title)
Bogdan Timofte authored 2 weeks ago
212
                    .font(.footnote.weight(.semibold))
213
                    .multilineTextAlignment(.center)
214
                    .lineLimit(2)
215
                    .minimumScaleFactor(0.9)
Bogdan Timofte authored 2 weeks ago
216
            }
Bogdan Timofte authored 2 weeks ago
217
            .foregroundColor(tint)
Bogdan Timofte authored 2 weeks ago
218
            .frame(width: width, height: actionButtonHeight)
Bogdan Timofte authored 2 weeks ago
219
            .contentShape(Rectangle())
Bogdan Timofte authored 2 weeks ago
220
        }
221
        .buttonStyle(.plain)
222
    }
223

            
Bogdan Timofte authored 2 weeks ago
224
    private var visibleActionButtonCount: CGFloat {
225
        meter.supportsRecordingView ? 3 : 2
226
    }
227

            
228
    private func actionButtonWidth(for availableWidth: CGFloat) -> CGFloat {
Bogdan Timofte authored 2 weeks ago
229
        let dividerWidth = actionDividerWidth * max(visibleActionButtonCount - 1, 0)
230
        let contentWidth = availableWidth - (actionStripPadding * 2) - dividerWidth
231
        let fittedWidth = floor(contentWidth / visibleActionButtonCount)
Bogdan Timofte authored 2 weeks ago
232
        return min(actionButtonMaxWidth, max(actionButtonMinWidth, fittedWidth))
233
    }
234

            
Bogdan Timofte authored 2 weeks ago
235
    private func actionStripWidth(for buttonWidth: CGFloat) -> CGFloat {
236
        let dividerWidth = actionDividerWidth * max(visibleActionButtonCount - 1, 0)
237
        return (buttonWidth * visibleActionButtonCount) + dividerWidth + (actionStripPadding * 2)
238
    }
239

            
240
    private var actionStripDivider: some View {
241
        Rectangle()
242
            .fill(Color.secondary.opacity(0.16))
243
            .frame(width: actionDividerWidth, height: actionButtonHeight - 24)
244
    }
245

            
246
    private var connectActionTint: Color {
247
        Color(red: 0.20, green: 0.46, blue: 0.43)
248
    }
249

            
250
    private var disconnectActionTint: Color {
251
        Color(red: 0.66, green: 0.39, blue: 0.35)
252
    }
253

            
Bogdan Timofte authored 2 weeks ago
254
    private var statusText: String {
255
        switch meter.operationalState {
256
        case .notPresent:
257
            return "Missing"
258
        case .peripheralNotConnected:
259
            return "Ready"
260
        case .peripheralConnectionPending:
261
            return "Connecting"
262
        case .peripheralConnected:
263
            return "Linked"
264
        case .peripheralReady:
265
            return "Preparing"
266
        case .comunicating:
267
            return "Syncing"
268
        case .dataIsAvailable:
269
            return "Live"
270
        }
271
    }
272

            
273
    private var statusColor: Color {
274
        Meter.operationalColor(for: meter.operationalState)
Bogdan Timofte authored 2 weeks ago
275
    }
Bogdan Timofte authored 2 weeks ago
276
}
Bogdan Timofte authored 2 weeks ago
277

            
278
private struct MeterInfoView: View {
279
    @EnvironmentObject private var meter: Meter
280

            
281
    var body: some View {
282
        ScrollView {
283
            VStack(spacing: 14) {
284
                MeterInfoCard(title: "Overview", tint: meter.color) {
285
                    MeterInfoRow(label: "Name", value: meter.name)
286
                    MeterInfoRow(label: "Displayed Model", value: meter.deviceModelSummary)
287
                    MeterInfoRow(label: "Advertised Model", value: meter.modelString)
288
                    MeterInfoRow(label: "Working Voltage", value: meter.documentedWorkingVoltage)
289
                    MeterInfoRow(label: "Temperature Unit", value: meter.temperatureUnitDescription)
290
                }
291

            
292
                MeterInfoCard(title: "Identifiers", tint: .blue) {
293
                    MeterInfoRow(label: "MAC", value: meter.btSerial.macAddress.description)
294
                    if meter.modelNumber != 0 {
295
                        MeterInfoRow(label: "Model Code", value: "\(meter.modelNumber)")
296
                    }
297
                }
298

            
Bogdan Timofte authored 2 weeks ago
299
                MeterInfoCard(title: "Screen Reporting", tint: .orange) {
300
                    if meter.reportsCurrentScreenIndex {
301
                        MeterInfoRow(label: "Current Screen", value: meter.currentScreenDescription)
302
                        Text("The active screen index is reported by the meter and mapped by the app to a known label.")
303
                            .font(.footnote)
304
                            .foregroundColor(.secondary)
305
                    } else {
306
                        MeterInfoRow(label: "Current Screen", value: "Not Reported")
307
                        Text("The current screen is not reported by the device payload, or we have not yet identified where and how the protocol announces it.")
308
                            .font(.footnote)
309
                            .foregroundColor(.secondary)
310
                    }
311
                }
312

            
Bogdan Timofte authored 2 weeks ago
313
                if meter.operationalState == .dataIsAvailable {
314
                    MeterInfoCard(title: "Live Device Details", tint: .indigo) {
315
                        if !meter.firmwareVersion.isEmpty {
316
                            MeterInfoRow(label: "Firmware", value: meter.firmwareVersion)
317
                        }
318
                        if meter.serialNumber != 0 {
319
                            MeterInfoRow(label: "Serial", value: "\(meter.serialNumber)")
320
                        }
321
                        if meter.bootCount != 0 {
322
                            MeterInfoRow(label: "Boot Count", value: "\(meter.bootCount)")
323
                        }
324
                    }
325
                } else {
326
                    MeterInfoCard(title: "Live Device Details", tint: .secondary) {
327
                        Text("Connect to the meter to load firmware, serial, and boot details.")
328
                            .font(.footnote)
329
                            .foregroundColor(.secondary)
330
                    }
331
                }
332
            }
333
            .padding()
334
        }
335
        .background(
336
            LinearGradient(
337
                colors: [meter.color.opacity(0.14), Color.clear],
338
                startPoint: .topLeading,
339
                endPoint: .bottomTrailing
340
            )
341
            .ignoresSafeArea()
342
        )
343
        .navigationBarTitle("Meter Info")
Bogdan Timofte authored 2 weeks ago
344
        .navigationBarItems(trailing: RSSIView(RSSI: meter.btSerial.RSSI).frame(width: 18, height: 18))
Bogdan Timofte authored 2 weeks ago
345
    }
346
}
347

            
348
private struct MeterInfoCard<Content: View>: View {
349
    let title: String
350
    let tint: Color
351
    @ViewBuilder var content: Content
352

            
353
    var body: some View {
354
        VStack(alignment: .leading, spacing: 12) {
355
            Text(title)
356
                .font(.headline)
357
            content
358
        }
359
        .frame(maxWidth: .infinity, alignment: .leading)
360
        .padding(18)
361
        .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.24)
362
    }
363
}
364

            
365
private struct MeterInfoRow: View {
366
    let label: String
367
    let value: String
368

            
369
    var body: some View {
370
        HStack {
371
            Text(label)
372
            Spacer()
373
            Text(value)
374
                .foregroundColor(.secondary)
375
                .multilineTextAlignment(.trailing)
376
        }
377
        .font(.footnote)
378
    }
379
}