Newer Older
320 lines | 12.097kb
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 }
21

            
Bogdan Timofte authored 2 weeks ago
22
    private let actionColumns = [
23
        GridItem(.adaptive(minimum: 112, maximum: 180), spacing: 12)
24
    ]
25

            
Bogdan Timofte authored 2 weeks ago
26
    var body: some View {
27
        ScrollView {
Bogdan Timofte authored 2 weeks ago
28
            VStack(alignment: .leading, spacing: 16) {
29
                headerCard
30
                connectionControlButton()
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
                    if meter.measurements.power.context.isValid {
36
                        MeasurementChartView()
37
                            .environmentObject(meter.measurements)
38
                            .frame(minHeight: myBounds.height / 3.4)
39
                            .padding(16)
40
                            .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20)
Bogdan Timofte authored 2 weeks ago
41
                    }
Bogdan Timofte authored 2 weeks ago
42

            
43
                    ControlView()
44
                        .padding(16)
45
                        .meterCard(tint: .indigo, fillOpacity: 0.12, strokeOpacity: 0.20)
46

            
47
                    LiveView()
48
                        .padding(18)
49
                        .meterCard(tint: meter.color, 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")
67
        .navigationBarItems(trailing: HStack (spacing: 0) {
68
            if meter.operationalState > .notPresent {
69
                RSSIView(RSSI: meter.btSerial.RSSI)
70
                    .frame(width: 24)
71
                    .padding(.vertical)
72
            }
Bogdan Timofte authored 2 weeks ago
73
            NavigationLink(destination: MeterInfoView().environmentObject(meter)) {
74
                Image(systemName: "info.circle.fill")
75
                    .padding(.vertical)
76
                    .padding(.leading)
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
                    .padding(.leading)
82
            }
83
        })
84
    }
Bogdan Timofte authored 2 weeks ago
85

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

            
115
    private var actionGrid: some View {
116
        LazyVGrid(columns: actionColumns, spacing: 12) {
117
            meterSheetButton(icon: "square.grid.2x2.fill", title: meter.dataGroupsTitle, tint: .teal) {
118
                dataGroupsViewVisibility.toggle()
119
            }
120
            .sheet(isPresented: $dataGroupsViewVisibility) {
121
                DataGroupsView(visibility: $dataGroupsViewVisibility)
122
                    .environmentObject(meter)
123
            }
124

            
125
            if meter.supportsRecordingView {
126
                meterSheetButton(icon: "gauge.with.dots.needle.50percent", title: "Charge Record", tint: .pink) {
127
                    recordingViewVisibility.toggle()
128
                }
129
                .sheet(isPresented: $recordingViewVisibility) {
130
                    RecordingView(visibility: $recordingViewVisibility)
131
                        .environmentObject(meter)
132
                }
133
            }
134

            
135
            meterSheetButton(icon: "clock.arrow.circlepath", title: "App History", tint: .blue) {
136
                measurementsViewVisibility.toggle()
137
            }
138
            .sheet(isPresented: $measurementsViewVisibility) {
139
                MeasurementsView(visibility: $measurementsViewVisibility)
140
                    .environmentObject(meter.measurements)
141
            }
142
        }
143
    }
Bogdan Timofte authored 2 weeks ago
144

            
145
    fileprivate func connectionControlButton() -> some View {
Bogdan Timofte authored 2 weeks ago
146
        let connected = meter.operationalState >= .peripheralConnectionPending
147
        let tint = connected ? Color.red : Color.green
148

            
Bogdan Timofte authored 2 weeks ago
149
        return Group {
150
            if meter.operationalState == .notPresent {
Bogdan Timofte authored 2 weeks ago
151
                HStack(spacing: 10) {
152
                    Image(systemName: "exclamationmark.triangle.fill")
153
                        .foregroundColor(.orange)
154
                    Text("Not found at this time.")
155
                        .fontWeight(.semibold)
156
                    Spacer()
157
                }
158
                .padding(16)
159
                .meterCard(tint: .orange, fillOpacity: 0.18, strokeOpacity: 0.24)
Bogdan Timofte authored 2 weeks ago
160
            } else {
Bogdan Timofte authored 2 weeks ago
161
                Button(action: {
Bogdan Timofte authored 2 weeks ago
162
                    if meter.operationalState < .peripheralConnectionPending {
Bogdan Timofte authored 2 weeks ago
163
                        meter.connect()
Bogdan Timofte authored 2 weeks ago
164
                    } else {
Bogdan Timofte authored 2 weeks ago
165
                        meter.disconnect()
166
                    }
167
                }) {
168
                    HStack {
169
                        Image(systemName: connected ? "xmark.circle.fill" : "bolt.horizontal.circle.fill")
170
                        Text(connected ? "Disconnect" : "Connect")
171
                            .fontWeight(.semibold)
172
                        Spacer()
173
                        Text(statusText)
174
                            .font(.footnote.weight(.medium))
175
                            .foregroundColor(.secondary)
Bogdan Timofte authored 2 weeks ago
176
                    }
Bogdan Timofte authored 2 weeks ago
177
                    .padding(.horizontal, 18)
178
                    .padding(.vertical, 16)
179
                    .frame(maxWidth: .infinity)
180
                    .foregroundColor(tint)
181
                    .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.24)
Bogdan Timofte authored 2 weeks ago
182
                }
Bogdan Timofte authored 2 weeks ago
183
                .buttonStyle(.plain)
Bogdan Timofte authored 2 weeks ago
184
            }
185
        }
186
    }
Bogdan Timofte authored 2 weeks ago
187

            
Bogdan Timofte authored 2 weeks ago
188
    fileprivate func meterSheetButton(icon: String, title: String, tint: Color, action: @escaping () -> Void) -> some View {
Bogdan Timofte authored 2 weeks ago
189
        Button(action: action) {
Bogdan Timofte authored 2 weeks ago
190
            VStack(spacing: 10) {
Bogdan Timofte authored 2 weeks ago
191
                Image(systemName: icon)
Bogdan Timofte authored 2 weeks ago
192
                    .font(.system(size: 20, weight: .semibold))
193
                    .frame(width: 40, height: 40)
194
                    .background(Circle().fill(tint.opacity(0.14)))
Bogdan Timofte authored 2 weeks ago
195
                Text(title)
Bogdan Timofte authored 2 weeks ago
196
                    .font(.footnote.weight(.semibold))
197
                    .multilineTextAlignment(.center)
198
                    .lineLimit(2)
199
                    .minimumScaleFactor(0.9)
Bogdan Timofte authored 2 weeks ago
200
            }
Bogdan Timofte authored 2 weeks ago
201
            .foregroundColor(tint)
202
            .frame(maxWidth: .infinity, minHeight: 106)
203
            .padding(.horizontal, 8)
204
            .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.22)
205
        }
206
        .buttonStyle(.plain)
207
    }
208

            
209
    private var statusText: String {
210
        switch meter.operationalState {
211
        case .notPresent:
212
            return "Missing"
213
        case .peripheralNotConnected:
214
            return "Ready"
215
        case .peripheralConnectionPending:
216
            return "Connecting"
217
        case .peripheralConnected:
218
            return "Linked"
219
        case .peripheralReady:
220
            return "Preparing"
221
        case .comunicating:
222
            return "Syncing"
223
        case .dataIsAvailable:
224
            return "Live"
225
        }
226
    }
227

            
228
    private var statusColor: Color {
229
        Meter.operationalColor(for: meter.operationalState)
Bogdan Timofte authored 2 weeks ago
230
    }
Bogdan Timofte authored 2 weeks ago
231
}
Bogdan Timofte authored 2 weeks ago
232

            
233
private struct MeterInfoView: View {
234
    @EnvironmentObject private var meter: Meter
235

            
236
    var body: some View {
237
        ScrollView {
238
            VStack(spacing: 14) {
239
                MeterInfoCard(title: "Overview", tint: meter.color) {
240
                    MeterInfoRow(label: "Name", value: meter.name)
241
                    MeterInfoRow(label: "Displayed Model", value: meter.deviceModelSummary)
242
                    MeterInfoRow(label: "Advertised Model", value: meter.modelString)
243
                    MeterInfoRow(label: "Working Voltage", value: meter.documentedWorkingVoltage)
244
                    MeterInfoRow(label: "Temperature Unit", value: meter.temperatureUnitDescription)
245
                }
246

            
247
                MeterInfoCard(title: "Identifiers", tint: .blue) {
248
                    MeterInfoRow(label: "MAC", value: meter.btSerial.macAddress.description)
249
                    if meter.modelNumber != 0 {
250
                        MeterInfoRow(label: "Model Code", value: "\(meter.modelNumber)")
251
                    }
252
                }
253

            
254
                if meter.operationalState == .dataIsAvailable {
255
                    MeterInfoCard(title: "Live Device Details", tint: .indigo) {
256
                        if !meter.firmwareVersion.isEmpty {
257
                            MeterInfoRow(label: "Firmware", value: meter.firmwareVersion)
258
                        }
259
                        if meter.serialNumber != 0 {
260
                            MeterInfoRow(label: "Serial", value: "\(meter.serialNumber)")
261
                        }
262
                        if meter.bootCount != 0 {
263
                            MeterInfoRow(label: "Boot Count", value: "\(meter.bootCount)")
264
                        }
265
                    }
266
                } else {
267
                    MeterInfoCard(title: "Live Device Details", tint: .secondary) {
268
                        Text("Connect to the meter to load firmware, serial, and boot details.")
269
                            .font(.footnote)
270
                            .foregroundColor(.secondary)
271
                    }
272
                }
273
            }
274
            .padding()
275
        }
276
        .background(
277
            LinearGradient(
278
                colors: [meter.color.opacity(0.14), Color.clear],
279
                startPoint: .topLeading,
280
                endPoint: .bottomTrailing
281
            )
282
            .ignoresSafeArea()
283
        )
284
        .navigationBarTitle("Meter Info")
285
        .navigationBarItems(trailing: RSSIView(RSSI: meter.btSerial.RSSI).frame(width: 24))
286
    }
287
}
288

            
289
private struct MeterInfoCard<Content: View>: View {
290
    let title: String
291
    let tint: Color
292
    @ViewBuilder var content: Content
293

            
294
    var body: some View {
295
        VStack(alignment: .leading, spacing: 12) {
296
            Text(title)
297
                .font(.headline)
298
            content
299
        }
300
        .frame(maxWidth: .infinity, alignment: .leading)
301
        .padding(18)
302
        .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.24)
303
    }
304
}
305

            
306
private struct MeterInfoRow: View {
307
    let label: String
308
    let value: String
309

            
310
    var body: some View {
311
        HStack {
312
            Text(label)
313
            Spacer()
314
            Text(value)
315
                .foregroundColor(.secondary)
316
                .multilineTextAlignment(.trailing)
317
        }
318
        .font(.footnote)
319
    }
320
}