Show private metadata and statistics for activity
This commit is contained in:
parent
26e06ffc06
commit
e5670afc22
@ -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 {
|
||||||
|
@ -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)")
|
||||||
|
Loading…
Reference in New Issue
Block a user