Add CSV export for days
This commit is contained in:
40
TempTrack/Storage/CsvConverter.swift
Normal file
40
TempTrack/Storage/CsvConverter.swift
Normal file
@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
|
||||
struct CsvConverter {
|
||||
|
||||
private let dateFormatter: DateFormatter
|
||||
|
||||
init() {
|
||||
dateFormatter = .init()
|
||||
dateFormatter.dateStyle = .short
|
||||
dateFormatter.timeStyle = .medium
|
||||
}
|
||||
|
||||
private var header: String {
|
||||
"Index;Date;Sensor 0;Sensor 1\n"
|
||||
}
|
||||
|
||||
func convert(measurements: [TemperatureMeasurement]) -> String {
|
||||
header + measurements.enumerated().map { (index, measurement) -> String in
|
||||
let parts: [String] = [
|
||||
"\(index)",
|
||||
dateFormatter.string(from: measurement.date),
|
||||
convert(measurement.sensor0),
|
||||
convert(measurement.sensor1)
|
||||
]
|
||||
|
||||
return parts.joined(separator: ";")
|
||||
}.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private func convert(_ value: TemperatureValue) -> String {
|
||||
switch value {
|
||||
case .notFound:
|
||||
return ""
|
||||
case .invalidMeasurement:
|
||||
return "?"
|
||||
case .value(let value):
|
||||
return String(format:"%.1f", value)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
private let df: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
@ -21,19 +22,45 @@ struct DayView: View {
|
||||
@EnvironmentObject
|
||||
var storage: PersistentStorage
|
||||
|
||||
@State
|
||||
private var showShareSheet = false
|
||||
|
||||
private let title: String
|
||||
|
||||
init(dateIndex: Int) {
|
||||
self.dateIndex = dateIndex
|
||||
self.title = Date(dateIndex: dateIndex)
|
||||
.formatted(date: .abbreviated, time: .omitted)
|
||||
}
|
||||
|
||||
var entries: [TemperatureMeasurement] {
|
||||
storage.loadMeasurements(for: dateIndex)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TemperatureDayOverview(points: entries)
|
||||
List(entries) { entry in
|
||||
HStack {
|
||||
Text(df.string(from: entry.date))
|
||||
Spacer()
|
||||
Text(entry.displayText)
|
||||
VStack {
|
||||
TemperatureDayOverview(points: entries)
|
||||
List(entries) { entry in
|
||||
HStack {
|
||||
Text(entry.date.formatted(date: .omitted, time: .standard))
|
||||
Spacer()
|
||||
Text(entry.displayText)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(title)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
Button(action: { showShareSheet = true }) {
|
||||
Label("Export", systemSymbol: .arrowUpDoc)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showShareSheet) {
|
||||
ShareCsvSheet(isPresented: $showShareSheet,
|
||||
measurements: entries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,7 @@ struct ExportSheet: View {
|
||||
Button("Delete archive", action: deleteArchive)
|
||||
.padding()
|
||||
}
|
||||
Button("Dismiss", action: { isPresented = false })
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
91
TempTrack/Views/ShareCsvSheet.swift
Normal file
91
TempTrack/Views/ShareCsvSheet.swift
Normal file
@ -0,0 +1,91 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ShareCsvSheet: View {
|
||||
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
@State
|
||||
private var isCreatingFile = false
|
||||
|
||||
@State var url: URL?
|
||||
|
||||
@State var error: String?
|
||||
|
||||
@EnvironmentObject
|
||||
private var storage: PersistentStorage
|
||||
|
||||
let measurements: [TemperatureMeasurement]
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Export")
|
||||
.font(.title)
|
||||
Text("Create a CSV file of all measurements for the day")
|
||||
if isCreatingFile {
|
||||
ProgressView()
|
||||
Text("Creating file...")
|
||||
}
|
||||
if let error {
|
||||
Text(error)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
Spacer()
|
||||
Button("Create file", action: createFile)
|
||||
.disabled(isCreatingFile)
|
||||
.padding()
|
||||
if let url {
|
||||
ShareLink(item: url) {
|
||||
Label("Share file", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
.padding()
|
||||
Button("Delete file", action: deleteFile)
|
||||
.padding()
|
||||
}
|
||||
Button("Dismiss", action: { isPresented = false })
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
private func createFile() {
|
||||
guard !isCreatingFile else {
|
||||
return
|
||||
}
|
||||
isCreatingFile = true
|
||||
DispatchQueue.main.async {
|
||||
do {
|
||||
let url = FileManager.default.temporaryDirectory.appendingPathComponent("export.csv")
|
||||
let converter = CsvConverter()
|
||||
let content = converter.convert(measurements: measurements)
|
||||
try content.write(to: url, atomically: true, encoding: .utf8)
|
||||
DispatchQueue.main.async {
|
||||
self.url = url
|
||||
self.error = nil
|
||||
self.isCreatingFile = false
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.error = "Failed to save file: \(error.localizedDescription)"
|
||||
self.isCreatingFile = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteFile() {
|
||||
guard let url else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
self.error = nil
|
||||
self.url = nil
|
||||
} catch {
|
||||
self.error = "Failed to delete archive: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ShareCsvSheet(isPresented: .constant(true),
|
||||
measurements: [])
|
||||
}
|
Reference in New Issue
Block a user