// // LiveView.swift // USB Meter // // Created by Bogdan Timofte on 09/03/2020. // Copyright © 2020 Bogdan Timofte. All rights reserved. // import SwiftUI struct LiveView: View { private struct MetricRange { let minLabel: String let maxLabel: String let minValue: String let maxValue: String } private struct LoadResistanceSymbol: View { let color: Color var body: some View { GeometryReader { proxy in let width = proxy.size.width let height = proxy.size.height let midY = height / 2 let startX = width * 0.10 let endX = width * 0.90 let boxMinX = width * 0.28 let boxMaxX = width * 0.72 let boxHeight = height * 0.34 let boxRect = CGRect( x: boxMinX, y: midY - (boxHeight / 2), width: boxMaxX - boxMinX, height: boxHeight ) let strokeWidth = max(1.2, height * 0.055) ZStack { Path { path in path.move(to: CGPoint(x: startX, y: midY)) path.addLine(to: CGPoint(x: boxRect.minX, y: midY)) path.move(to: CGPoint(x: boxRect.maxX, y: midY)) path.addLine(to: CGPoint(x: endX, y: midY)) } .stroke( color, style: StrokeStyle( lineWidth: strokeWidth, lineCap: .round, lineJoin: .round ) ) Path { path in path.addRect(boxRect) } .stroke( color, style: StrokeStyle( lineWidth: strokeWidth, lineCap: .round, lineJoin: .round ) ) } } .padding(4) } } @EnvironmentObject private var meter: Meter var compactLayout: Bool = false var availableSize: CGSize? = nil var body: some View { VStack(alignment: .leading, spacing: 16) { HStack { Text("Live Data") .font(.headline) Spacer() statusBadge } LazyVGrid(columns: liveMetricColumns, spacing: compactLayout ? 10 : 12) { liveMetricCard( title: "Voltage", symbol: "bolt.fill", color: .green, value: "\(meter.voltage.format(decimalDigits: 3)) V", range: metricRange( min: meter.measurements.voltage.context.minValue, max: meter.measurements.voltage.context.maxValue, unit: "V" ) ) liveMetricCard( title: "Current", symbol: "waveform.path.ecg", color: .blue, value: "\(meter.current.format(decimalDigits: 3)) A", range: metricRange( min: meter.measurements.current.context.minValue, max: meter.measurements.current.context.maxValue, unit: "A" ) ) liveMetricCard( title: "Power", symbol: "flame.fill", color: .pink, value: "\(meter.power.format(decimalDigits: 3)) W", range: metricRange( min: meter.measurements.power.context.minValue, max: meter.measurements.power.context.maxValue, unit: "W" ) ) liveMetricCard( title: "Temperature", symbol: "thermometer.medium", color: .orange, value: meter.primaryTemperatureDescription, range: temperatureRange() ) liveMetricCard( title: "Load", customSymbol: AnyView(LoadResistanceSymbol(color: .yellow)), color: .yellow, value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}", detailText: "Measured resistance" ) liveMetricCard( title: "RSSI", symbol: "dot.radiowaves.left.and.right", color: .mint, value: "\(meter.btSerial.RSSI) dBm", range: MetricRange( minLabel: "Min", maxLabel: "Max", minValue: "\(meter.btSerial.minRSSI) dBm", maxValue: "\(meter.btSerial.maxRSSI) dBm" ), valueFont: .system(compactLayout ? .subheadline : .headline, design: .rounded).weight(.bold) ) } } .frame(maxWidth: .infinity, alignment: .topLeading) } private var liveMetricColumns: [GridItem] { if compactLayout { return Array(repeating: GridItem(.flexible(), spacing: 10), count: 3) } return [GridItem(.flexible()), GridItem(.flexible())] } private var statusBadge: some View { Text(meter.operationalState == .dataIsAvailable ? "Live" : "Waiting") .font(.caption.weight(.semibold)) .padding(.horizontal, 10) .padding(.vertical, 6) .foregroundColor(meter.operationalState == .dataIsAvailable ? .green : .secondary) .meterCard( tint: meter.operationalState == .dataIsAvailable ? .green : .secondary, fillOpacity: 0.12, strokeOpacity: 0.16, cornerRadius: 999 ) } private var showsCompactMetricRange: Bool { compactLayout && (availableSize?.height ?? 0) >= 380 } private var shouldShowMetricRange: Bool { !compactLayout || showsCompactMetricRange } private func liveMetricCard( title: String, symbol: String? = nil, customSymbol: AnyView? = nil, color: Color, value: String, range: MetricRange? = nil, detailText: String? = nil, valueFont: Font? = nil, valueLineLimit: Int = 1, valueMonospacedDigits: Bool = true, valueMinimumScaleFactor: CGFloat = 0.85 ) -> some View { VStack(alignment: .leading, spacing: 10) { HStack(spacing: compactLayout ? 8 : 10) { Group { if let customSymbol { customSymbol } else if let symbol { Image(systemName: symbol) .font(.system(size: compactLayout ? 14 : 15, weight: .semibold)) .foregroundColor(color) } } .frame(width: compactLayout ? 30 : 34, height: compactLayout ? 30 : 34) .background(Circle().fill(color.opacity(0.12))) Text(title) .font((compactLayout ? Font.caption : .subheadline).weight(.semibold)) .foregroundColor(.secondary) .lineLimit(1) Spacer(minLength: 0) } Group { if valueMonospacedDigits { Text(value) .monospacedDigit() } else { Text(value) } } .font(valueFont ?? .system(compactLayout ? .headline : .title3, design: .rounded).weight(.bold)) .lineLimit(valueLineLimit) .minimumScaleFactor(valueMinimumScaleFactor) if shouldShowMetricRange { if let range { metricRangeTable(range) } else if let detailText, !detailText.isEmpty { Text(detailText) .font(.caption) .foregroundColor(.secondary) .lineLimit(2) } } } .frame( maxWidth: .infinity, minHeight: compactLayout ? (shouldShowMetricRange ? 132 : 96) : 128, alignment: .leading ) .padding(compactLayout ? 12 : 16) .meterCard(tint: color, fillOpacity: 0.10, strokeOpacity: 0.12) } private func metricRangeTable(_ range: MetricRange) -> some View { VStack(alignment: .leading, spacing: 4) { HStack(spacing: 12) { Text(range.minLabel) Spacer(minLength: 0) Text(range.maxLabel) } .font(.caption2.weight(.semibold)) .foregroundColor(.secondary) HStack(spacing: 12) { Text(range.minValue) .monospacedDigit() Spacer(minLength: 0) Text(range.maxValue) .monospacedDigit() } .font(.caption.weight(.medium)) .foregroundColor(.primary) } } private func metricRange(min: Double, max: Double, unit: String) -> MetricRange? { guard min.isFinite, max.isFinite else { return nil } return MetricRange( minLabel: "Min", maxLabel: "Max", minValue: "\(min.format(decimalDigits: 3)) \(unit)", maxValue: "\(max.format(decimalDigits: 3)) \(unit)" ) } private func temperatureRange() -> MetricRange? { let value = meter.primaryTemperatureDescription guard !value.isEmpty else { return nil } return MetricRange( minLabel: "Min", maxLabel: "Max", minValue: value, maxValue: value ) } }