Show private metadata and statistics for activity

This commit is contained in:
Christoph Hagen 2024-03-20 10:19:08 +01:00
parent 26e06ffc06
commit e5670afc22
2 changed files with 71 additions and 41 deletions

View File

@ -13,9 +13,17 @@ struct ActivityDetailView: View {
let activity: HKWorkoutActivity 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)] { private var metadata: [(key: String, value: Any)] {
activity.metadata?.sorted { $0.key } ?? [] activity.metadata?.sorted { $0.key } ?? []
@ -33,22 +41,12 @@ struct ActivityDetailView: View {
DetailRow("End", date: activity.endDate) DetailRow("End", date: activity.endDate)
DetailRow("Duration", duration: activity.duration) DetailRow("Duration", duration: activity.duration)
} }
Section("Data") { if !statistics.isEmpty {
if !locations.isEmpty { Section("Statistics") {
NavigationLink(value: locations) { ForEach(statistics, id: \.type) { (type, statistic) in
DetailRow("Locations", value: "\(locations.count)") 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) { 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") .navigationTitle("Activity")
.navigationDestination(for: [CLLocation].self) { locations in
LocationSampleListView(samples: locations)
}
.onAppear(perform: load) .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() { private func load() {
@ -71,22 +85,36 @@ struct ActivityDetailView: View {
return return
} }
Task { Task {
do { await loadStatistics(db: store)
guard let route = try store.route(associatedWith: workout) else { await loadPrivateMetadata(db: store)
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)")
}
} }
} }
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 { #Preview {

View File

@ -29,7 +29,7 @@ struct WorkoutDetailView: View {
private var locationSamples: [CLLocation] = [] private var locationSamples: [CLLocation] = []
@State @State
private var privateMetadata: [String : Any] = [:] private var privateMetadata: [(key: String, value: Any)] = []
@State @State
private var showErrorMessage = false private var showErrorMessage = false
@ -44,10 +44,6 @@ struct WorkoutDetailView: View {
workout.metadata.sorted { $0.key } workout.metadata.sorted { $0.key }
} }
private var privateMetadataFields: [(key: String, value: Any)] {
privateMetadata.sorted { $0.key }
}
private var averageHeartRate: Int { private var averageHeartRate: Int {
let sum = heartRateSamples.reduce(0) { $0 + $1.beatsPerMinute } let sum = heartRateSamples.reduce(0) { $0 + $1.beatsPerMinute }
return (Double(sum) / Double(heartRateSamples.count)).roundedInt return (Double(sum) / Double(heartRateSamples.count)).roundedInt
@ -122,7 +118,7 @@ struct WorkoutDetailView: View {
DetailRow("Metadata", value: workout.metadata.count) DetailRow("Metadata", value: workout.metadata.count)
} }
DisclosureGroup { DisclosureGroup {
ForEach(privateMetadataFields, id:\.key) { (key, value) in ForEach(privateMetadata, id:\.key) { (key, value) in
DetailRow(MetadataKeyName(key), value: "\(value)") DetailRow(MetadataKeyName(key), value: "\(value)")
} }
} label: { } label: {
@ -152,6 +148,9 @@ struct WorkoutDetailView: View {
Text("") Text("")
.frame(height: 150) .frame(height: 150)
.listRowBackground(WorkoutMapView(locations: locationSamples)) .listRowBackground(WorkoutMapView(locations: locationSamples))
.overlay(
NavigationLink(value: locationSamples) { }
.opacity(0))
} }
} }
} }
@ -163,6 +162,9 @@ struct WorkoutDetailView: View {
.navigationDestination(for: HKWorkoutEvent.self) { event in .navigationDestination(for: HKWorkoutEvent.self) { event in
EventDetailView(event: event) EventDetailView(event: event)
} }
.navigationDestination(for: [CLLocation].self) { locations in
LocationSampleListView(samples: locations)
}
.onAppear(perform: loadSamples) .onAppear(perform: loadSamples)
.alert("Processing error", isPresented: $showErrorMessage) { .alert("Processing error", isPresented: $showErrorMessage) {
Button("Dismiss", role: .cancel) { } Button("Dismiss", role: .cancel) { }
@ -305,7 +307,7 @@ struct WorkoutDetailView: View {
let metadata = try db.store.metadata(for: workout.uuid, includePrivateMetadata: true) let metadata = try db.store.metadata(for: workout.uuid, includePrivateMetadata: true)
.filter { $0.key.hasPrefix("_HKPrivate") } .filter { $0.key.hasPrefix("_HKPrivate") }
DispatchQueue.main.async { DispatchQueue.main.async {
self.privateMetadata = metadata self.privateMetadata = metadata.sorted { $0.key }
} }
} catch { } catch {
show("Failed to load private metadata from database: \(error)") show("Failed to load private metadata from database: \(error)")