Newer Older
249 lines | 9.449kb
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
            }
73
            NavigationLink(destination: MeterSettingsView().environmentObject(meter)) {
Bogdan Timofte authored 2 weeks ago
74
                Image(systemName: "gearshape.fill")
Bogdan Timofte authored 2 weeks ago
75
                    .padding(.vertical)
76
                    .padding(.leading)
77
            }
78
        })
79
    }
Bogdan Timofte authored 2 weeks ago
80

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

            
106
            Divider().opacity(0.35)
107

            
108
            HStack(spacing: 12) {
109
                headerPill(title: "MAC", value: meter.btSerial.macAddress.description)
110
                headerPill(title: "Range", value: meter.documentedWorkingVoltage)
111
            }
112
        }
113
        .padding(20)
114
        .meterCard(tint: meter.color, fillOpacity: 0.22, strokeOpacity: 0.24)
115
    }
116

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

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

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

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

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

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

            
211
    private func headerPill(title: String, value: String) -> some View {
212
        VStack(alignment: .leading, spacing: 4) {
213
            Text(title)
214
                .font(.caption.weight(.semibold))
215
                .foregroundColor(.secondary)
216
            Text(value)
217
                .font(.footnote.weight(.semibold))
218
                .lineLimit(1)
219
                .minimumScaleFactor(0.8)
Bogdan Timofte authored 2 weeks ago
220
        }
Bogdan Timofte authored 2 weeks ago
221
        .frame(maxWidth: .infinity, alignment: .leading)
222
        .padding(.horizontal, 12)
223
        .padding(.vertical, 10)
224
        .meterCard(tint: meter.color, fillOpacity: 0.16, strokeOpacity: 0.20, cornerRadius: 16)
225
    }
226

            
227
    private var statusText: String {
228
        switch meter.operationalState {
229
        case .notPresent:
230
            return "Missing"
231
        case .peripheralNotConnected:
232
            return "Ready"
233
        case .peripheralConnectionPending:
234
            return "Connecting"
235
        case .peripheralConnected:
236
            return "Linked"
237
        case .peripheralReady:
238
            return "Preparing"
239
        case .comunicating:
240
            return "Syncing"
241
        case .dataIsAvailable:
242
            return "Live"
243
        }
244
    }
245

            
246
    private var statusColor: Color {
247
        Meter.operationalColor(for: meter.operationalState)
Bogdan Timofte authored 2 weeks ago
248
    }
Bogdan Timofte authored 2 weeks ago
249
}