From 8b4ebf464578cbef6013014564f68c3a16aab3d0 Mon Sep 17 00:00:00 2001 From: jake-b <1012393+jake-b@users.noreply.github.com> Date: Tue, 9 Sep 2025 20:24:44 -0400 Subject: [PATCH] TableColumnForEach for dynamic EnvironmentMetricsLog columns (#1384) * TableColumnForEach implementation for Mac Catalyst * Moved EnvironmentMetricsLog to @FetchRequest --------- Co-authored-by: Jake-B --- Meshtastic/Export/WriteCsvFile.swift | 2 +- .../MetricsSeriesList.swift | 2 +- .../Views/Nodes/EnvironmentMetricsLog.swift | 55 ++++++++---------- .../Metrics Columns/MetricsColumnDetail.swift | 58 ++++++++++++------- 4 files changed, 63 insertions(+), 54 deletions(-) diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index badc6f9b..f54e3ae4 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -8,7 +8,7 @@ import SwiftUI import OSLog -func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> String { +func telemetryToCsvFile(telemetry: S, metricsType: Int) -> String where S.Element == TelemetryEntity { var csvString: String = "" let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") diff --git a/Meshtastic/Model/Metrics Visualization/MetricsSeriesList.swift b/Meshtastic/Model/Metrics Visualization/MetricsSeriesList.swift index 1def5f47..52000d3f 100644 --- a/Meshtastic/Model/Metrics Visualization/MetricsSeriesList.swift +++ b/Meshtastic/Model/Metrics Visualization/MetricsSeriesList.swift @@ -44,7 +44,7 @@ class MetricsSeriesList: ObservableObject, RandomAccessCollection, RangeReplacea // configuraiton, such as: // 1. starting with a desired fixed range // 2. obeying a minimum span - func chartRange(forData data: [TelemetryEntity]) -> ClosedRange { + func chartRange(forData data: S) -> ClosedRange where S.Element == TelemetryEntity { var globalLower: Float = .infinity var globalUpper: Float = -.infinity diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index be081e84..79aa8d8b 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -7,6 +7,7 @@ import SwiftUI import Charts import OSLog +import CoreData struct EnvironmentMetricsLog: View { @@ -21,19 +22,27 @@ struct EnvironmentMetricsLog: View { @StateObject var seriesList = MetricsSeriesList.environmentDefaultChartSeries @State var isEditingColumnConfiguration = false - + + @FetchRequest private var chartData: FetchedResults + + init(node: NodeInfoEntity) { + self.node = node + + // Build fetch request: + let request: NSFetchRequest = TelemetryEntity.fetchRequest() + let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date.distantPast + request.predicate = NSPredicate(format: "nodeTelemetry == %@ AND metricsType == 1 AND time >= %@", node, oneWeekAgo as NSDate) + request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] + _chartData = FetchRequest(fetchRequest: request) + } + var body: some View { VStack { if node.hasEnvironmentMetrics { - let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) - let environmentMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).reversed() as? [TelemetryEntity] ?? [] - let chartData = environmentMetrics - .filter { $0.time != nil && $0.time! >= oneWeekAgo! } - .sorted { $0.time! < $1.time! } let chartRange = applyMargins(seriesList.chartRange(forData: chartData)) VStack { if chartData.count > 0 { - GroupBox(label: Label("\(environmentMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) { + GroupBox(label: Label("\(chartData.count) Readings Total", systemImage: "chart.xyaxis.line")) { Chart(seriesList.visible) { series in ForEach(chartData, id: \.time) { dataPoint in series.body(dataPoint, inChartRange: chartRange) @@ -54,29 +63,12 @@ struct EnvironmentMetricsLog: View { // to be bumped to 17.4 -- Until that happens, the existing non-configurable table is used. if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { // Add a table for mac and ipad - Table(environmentMetrics) { - TableColumn("Temperature") { em in - columnList.column(withId: "temperature")?.body(em) + Table(chartData) { + TableColumnForEach(columnList.visible) { col in + TableColumn(col.name) { em in + col.body(em) + } } - TableColumn("Humidity") { em in - columnList.column(withId: "relativeHumidity")?.body(em) - } - TableColumn("Barometric Pressure") { em in - columnList.column(withId: "barometricPressure")?.body(em) - } - TableColumn("Indoor Air Quality") { em in - columnList.column(withId: "iaq")?.body(em) - } - TableColumn("Wind Speed") { em in - columnList.column(withId: "windSpeed")?.body(em) - } - TableColumn("Wind Direction") { em in - columnList.column(withId: "windDirection")?.body(em) - } - TableColumn("Timestamp") { em in - columnList.column(withId: "time")?.body(em) - } - .width(min: 180) } } else { ScrollView { @@ -88,7 +80,7 @@ struct EnvironmentMetricsLog: View { .fontWeight(.bold) } } - ForEach(environmentMetrics, id: \.self) { em in + ForEach(chartData) { em in GridRow { ForEach(columnList.visible) { col in col.body(em) @@ -142,7 +134,7 @@ struct EnvironmentMetricsLog: View { } } Button { - exportString = telemetryToCsvFile(telemetry: environmentMetrics, metricsType: 1) + exportString = telemetryToCsvFile(telemetry: chartData, metricsType: 1) isExporting = true } label: { Label("Save", systemImage: "square.and.arrow.down") @@ -192,3 +184,4 @@ struct EnvironmentMetricsLog: View { return lower...upper } } + diff --git a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift index dbe3303e..e456ce31 100644 --- a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift @@ -14,11 +14,31 @@ struct MetricsColumnDetail: View { @State private var currentDetent = PresentationDetent.medium @Environment(\.dismiss) private var dismiss - + + enum ViewOption: String, CaseIterable, Identifiable { + case chart = "Chart" + case table = "Table" + + var id: String { rawValue } + } + + @State private var selectedView: ViewOption = .chart + var body: some View { NavigationStack { Form { - Section("Chart") { + Section { + Picker("", selection: $selectedView) { + ForEach(ViewOption.allCases) { option in + Text(option.rawValue) + .tag(option) + } + } + .pickerStyle(.segmented) + }.listRowBackground(Color.clear) + + switch selectedView { + case .chart: ForEach(seriesList) { series in HStack { Path { path in @@ -40,29 +60,25 @@ struct MetricsColumnDetail: View { seriesList.toggleVisibity(for: series) } } - } - // Dynamic table column using SwiftUI Table requires TableColumnForEach which requires the target - // to be bumped to 17.4 -- Until that happens, the existing non-configurable table is used. - if !(UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac) { - Section("Table") { - ForEach(columnList.columns) { column in - HStack { - Text(column.name) - Spacer() - if column.visible { - Image(systemName: "checkmark") - .foregroundColor(.blue) - } - }.contentShape(Rectangle()) // Ensures the entire row is tappable - .onTapGesture { - columnList.objectWillChange.send() - columnList.toggleVisibity(for: column) - } - } + case .table: + ForEach(columnList.columns) { column in + HStack { + Text(column.name) + Spacer() + if column.visible { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + }.contentShape(Rectangle()) // Ensures the entire row is tappable + .onTapGesture { + columnList.objectWillChange.send() + columnList.toggleVisibity(for: column) + } } } } .listStyle(.insetGrouped) + .listSectionSpacing(12) #if targetEnvironment(macCatalyst) Spacer() Button {