From e5670afc2243cb8fec559aecfb407b7aa08e93b1 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Wed, 20 Mar 2024 10:19:08 +0100 Subject: [PATCH] Show private metadata and statistics for activity --- HealthImport/ActivityDetailView.swift | 96 +++++++++++++++++---------- HealthImport/WorkoutDetailView.swift | 16 +++-- 2 files changed, 71 insertions(+), 41 deletions(-) diff --git a/HealthImport/ActivityDetailView.swift b/HealthImport/ActivityDetailView.swift index 43adb89..cdf6dd4 100644 --- a/HealthImport/ActivityDetailView.swift +++ b/HealthImport/ActivityDetailView.swift @@ -13,9 +13,17 @@ struct ActivityDetailView: View { let activity: HKWorkoutActivity - @State var locations: [CLLocation] = [] + @State + private var statistics: [(type: HKQuantityType, statistics: Statistics)] = [] - @State var sampleCount: Int = 0 + @State + private var privateMetadata: [(key: String, value: Any)] = [] + + @State + private var showErrorMessage = false + + @State + private var errorMessage: String = "" private var metadata: [(key: String, value: Any)] { activity.metadata?.sorted { $0.key } ?? [] @@ -33,22 +41,12 @@ struct ActivityDetailView: View { DetailRow("End", date: activity.endDate) DetailRow("Duration", duration: activity.duration) } - Section("Data") { - if !locations.isEmpty { - NavigationLink(value: locations) { - DetailRow("Locations", value: "\(locations.count)") + if !statistics.isEmpty { + Section("Statistics") { + ForEach(statistics, id: \.type) { (type, statistic) in + let name = HKQuantityTypeIdentifier(rawValue: type.identifier).description + DetailRow(name, value: statistic.average) } - } else { - DetailRow("Locations", value: "0") - } - if sampleCount != 0 { - NavigationLink { - ActivitySamplesView(activity: activity) - } label: { - DetailRow("Samples", value: "\(sampleCount)") - } - } else { - DetailRow("Samples", value: "0") } } if !(activity.metadata?.isEmpty ?? true) { @@ -58,12 +56,28 @@ struct ActivityDetailView: View { } } } + if !privateMetadata.isEmpty { + Section("Private Metadata") { + ForEach(privateMetadata, id:\.key) { (key, value) in + DetailRow(MetadataKeyName(key), value: "\(value)") + } + } + } } .navigationTitle("Activity") - .navigationDestination(for: [CLLocation].self) { locations in - LocationSampleListView(samples: locations) - } .onAppear(perform: load) + .alert("Processing error", isPresented: $showErrorMessage) { + Button("Dismiss", role: .cancel) { } + } message: { + Text(errorMessage) + } + } + + private func show(_ error: String) { + DispatchQueue.main.async { + self.errorMessage = error + self.showErrorMessage = true + } } private func load() { @@ -71,22 +85,36 @@ struct ActivityDetailView: View { return } Task { - do { - guard let route = try store.route(associatedWith: workout) else { - return - } - let samples = try store.locations(associatedWith: route) - .sorted { $0.timestamp } - //let sampleCount = try HealthDatabase.shared.sampleCount(for: activity) - DispatchQueue.main.async { - self.locations = samples - //self.sampleCount = sampleCount - } - } catch { - print("Failed to load location samples for activity: \(error)") - } + await loadStatistics(db: store) + await loadPrivateMetadata(db: store) } } + + private func loadStatistics(db: HealthDatabase) async { + do { + let statistics = try db.store.statistics(associatedWith: activity) + DispatchQueue.main.async { + self.statistics = statistics + .sorted { $0.key.description } + .map { ($0, $1) } + } + } catch { + print("Failed to load statistics from database: \(error)") + } + } + + private func loadPrivateMetadata(db: HealthDatabase) async { + do { + let metadata = try db.store.metadata(for: workout.uuid, includePrivateMetadata: true) + .filter { $0.key.hasPrefix("_HKPrivate") } + DispatchQueue.main.async { + self.privateMetadata = metadata.sorted { $0.key } + } + } catch { + show("Failed to load private metadata from database: \(error)") + } + } + } #Preview { diff --git a/HealthImport/WorkoutDetailView.swift b/HealthImport/WorkoutDetailView.swift index d51e744..31a33df 100644 --- a/HealthImport/WorkoutDetailView.swift +++ b/HealthImport/WorkoutDetailView.swift @@ -29,7 +29,7 @@ struct WorkoutDetailView: View { private var locationSamples: [CLLocation] = [] @State - private var privateMetadata: [String : Any] = [:] + private var privateMetadata: [(key: String, value: Any)] = [] @State private var showErrorMessage = false @@ -44,10 +44,6 @@ struct WorkoutDetailView: View { workout.metadata.sorted { $0.key } } - private var privateMetadataFields: [(key: String, value: Any)] { - privateMetadata.sorted { $0.key } - } - private var averageHeartRate: Int { let sum = heartRateSamples.reduce(0) { $0 + $1.beatsPerMinute } return (Double(sum) / Double(heartRateSamples.count)).roundedInt @@ -122,7 +118,7 @@ struct WorkoutDetailView: View { DetailRow("Metadata", value: workout.metadata.count) } DisclosureGroup { - ForEach(privateMetadataFields, id:\.key) { (key, value) in + ForEach(privateMetadata, id:\.key) { (key, value) in DetailRow(MetadataKeyName(key), value: "\(value)") } } label: { @@ -152,6 +148,9 @@ struct WorkoutDetailView: View { Text("") .frame(height: 150) .listRowBackground(WorkoutMapView(locations: locationSamples)) + .overlay( + NavigationLink(value: locationSamples) { } + .opacity(0)) } } } @@ -163,6 +162,9 @@ struct WorkoutDetailView: View { .navigationDestination(for: HKWorkoutEvent.self) { event in EventDetailView(event: event) } + .navigationDestination(for: [CLLocation].self) { locations in + LocationSampleListView(samples: locations) + } .onAppear(perform: loadSamples) .alert("Processing error", isPresented: $showErrorMessage) { Button("Dismiss", role: .cancel) { } @@ -305,7 +307,7 @@ struct WorkoutDetailView: View { let metadata = try db.store.metadata(for: workout.uuid, includePrivateMetadata: true) .filter { $0.key.hasPrefix("_HKPrivate") } DispatchQueue.main.async { - self.privateMetadata = metadata + self.privateMetadata = metadata.sorted { $0.key } } } catch { show("Failed to load private metadata from database: \(error)")