// // MeterView.swift // USB Meter // // Created by Bogdan Timofte on 04/03/2020. // Copyright © 2020 Bogdan Timofte. All rights reserved. // // MARK: Parent frame https://stackoverflow.com/questions/56832865/how-to-access-parents-frame-in-swiftui import SwiftUI import CoreBluetooth struct MeterView: View { @EnvironmentObject private var meter: Meter @State var dataGroupsViewVisibility: Bool = false @State var recordingViewVisibility: Bool = false @State var measurementsViewVisibility: Bool = false private var myBounds: CGRect { UIScreen.main.bounds } private let actionColumns = [ GridItem(.adaptive(minimum: 112, maximum: 180), spacing: 12) ] var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { headerCard connectionControlButton() if meter.operationalState == .dataIsAvailable { actionGrid if meter.measurements.power.context.isValid { MeasurementChartView() .environmentObject(meter.measurements) .frame(minHeight: myBounds.height / 3.4) .padding(16) .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20) } ControlView() .padding(16) .meterCard(tint: .indigo, fillOpacity: 0.12, strokeOpacity: 0.20) LiveView() .padding(18) .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20) } } .padding() } .background( LinearGradient( colors: [ meter.color.opacity(0.22), Color.secondary.opacity(0.08), Color.clear ], startPoint: .topLeading, endPoint: .bottomTrailing ) .ignoresSafeArea() ) .navigationBarTitle("Meter") .navigationBarItems(trailing: HStack (spacing: 0) { if meter.operationalState > .notPresent { RSSIView(RSSI: meter.btSerial.RSSI) .frame(width: 24) .padding(.vertical) } NavigationLink(destination: MeterSettingsView().environmentObject(meter)) { Image(systemName: "gearshape.fill") .padding(.vertical) .padding(.leading) } }) } private var headerCard: some View { VStack(alignment: .leading, spacing: 14) { HStack(alignment: .top) { VStack(alignment: .leading, spacing: 6) { Text(meter.name) .font(.system(.title2, design: .rounded).weight(.bold)) Text(meter.deviceModelSummary) .font(.subheadline.weight(.semibold)) .foregroundColor(.secondary) } Spacer() VStack(alignment: .trailing, spacing: 6) { Text(statusText) .font(.caption.weight(.bold)) .padding(.horizontal, 12) .padding(.vertical, 6) .meterCard(tint: statusColor, fillOpacity: 0.24, strokeOpacity: 0.30, cornerRadius: 999) if meter.operationalState > .notPresent { Text("RSSI \(meter.btSerial.RSSI)") .font(.caption) .foregroundColor(.secondary) } } } Divider().opacity(0.35) HStack(spacing: 12) { headerPill(title: "MAC", value: meter.btSerial.macAddress.description) headerPill(title: "Range", value: meter.documentedWorkingVoltage) } } .padding(20) .meterCard(tint: meter.color, fillOpacity: 0.22, strokeOpacity: 0.24) } private var actionGrid: some View { LazyVGrid(columns: actionColumns, spacing: 12) { meterSheetButton(icon: "square.grid.2x2.fill", title: meter.dataGroupsTitle, tint: .teal) { dataGroupsViewVisibility.toggle() } .sheet(isPresented: $dataGroupsViewVisibility) { DataGroupsView(visibility: $dataGroupsViewVisibility) .environmentObject(meter) } if meter.supportsRecordingView { meterSheetButton(icon: "gauge.with.dots.needle.50percent", title: "Charge Record", tint: .pink) { recordingViewVisibility.toggle() } .sheet(isPresented: $recordingViewVisibility) { RecordingView(visibility: $recordingViewVisibility) .environmentObject(meter) } } meterSheetButton(icon: "clock.arrow.circlepath", title: "App History", tint: .blue) { measurementsViewVisibility.toggle() } .sheet(isPresented: $measurementsViewVisibility) { MeasurementsView(visibility: $measurementsViewVisibility) .environmentObject(meter.measurements) } } } fileprivate func connectionControlButton() -> some View { let connected = meter.operationalState >= .peripheralConnectionPending let tint = connected ? Color.red : Color.green return Group { if meter.operationalState == .notPresent { HStack(spacing: 10) { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.orange) Text("Not found at this time.") .fontWeight(.semibold) Spacer() } .padding(16) .meterCard(tint: .orange, fillOpacity: 0.18, strokeOpacity: 0.24) } else { Button(action: { if meter.operationalState < .peripheralConnectionPending { meter.connect() } else { meter.disconnect() } }) { HStack { Image(systemName: connected ? "xmark.circle.fill" : "bolt.horizontal.circle.fill") Text(connected ? "Disconnect" : "Connect") .fontWeight(.semibold) Spacer() Text(statusText) .font(.footnote.weight(.medium)) .foregroundColor(.secondary) } .padding(.horizontal, 18) .padding(.vertical, 16) .frame(maxWidth: .infinity) .foregroundColor(tint) .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.24) } .buttonStyle(.plain) } } } fileprivate func meterSheetButton(icon: String, title: String, tint: Color, action: @escaping () -> Void) -> some View { Button(action: action) { VStack(spacing: 10) { Image(systemName: icon) .font(.system(size: 20, weight: .semibold)) .frame(width: 40, height: 40) .background(Circle().fill(tint.opacity(0.14))) Text(title) .font(.footnote.weight(.semibold)) .multilineTextAlignment(.center) .lineLimit(2) .minimumScaleFactor(0.9) } .foregroundColor(tint) .frame(maxWidth: .infinity, minHeight: 106) .padding(.horizontal, 8) .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.22) } .buttonStyle(.plain) } private func headerPill(title: String, value: String) -> some View { VStack(alignment: .leading, spacing: 4) { Text(title) .font(.caption.weight(.semibold)) .foregroundColor(.secondary) Text(value) .font(.footnote.weight(.semibold)) .lineLimit(1) .minimumScaleFactor(0.8) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 12) .padding(.vertical, 10) .meterCard(tint: meter.color, fillOpacity: 0.16, strokeOpacity: 0.20, cornerRadius: 16) } private var statusText: String { switch meter.operationalState { case .notPresent: return "Missing" case .peripheralNotConnected: return "Ready" case .peripheralConnectionPending: return "Connecting" case .peripheralConnected: return "Linked" case .peripheralReady: return "Preparing" case .comunicating: return "Syncing" case .dataIsAvailable: return "Live" } } private var statusColor: Color { Meter.operationalColor(for: meter.operationalState) } }