1 contributor
//
// 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
)
}
}