Allow deletion of workouts

This commit is contained in:
Christoph Hagen 2024-03-20 14:48:01 +01:00
parent 7e66f81aa6
commit ee1993e757

View File

@ -40,6 +40,9 @@ struct WorkoutDetailView: View {
@State @State
private var isProcessingWorkout = false private var isProcessingWorkout = false
@State
private var healthButtonText = "Checking for workout in Health..."
private var metadataFields: [(key: String, value: Any)] { private var metadataFields: [(key: String, value: Any)] {
workout.metadata.sorted { $0.key } workout.metadata.sorted { $0.key }
} }
@ -51,37 +54,28 @@ struct WorkoutDetailView: View {
var body: some View { var body: some View {
List { List {
if healthWorkout != nil { Button(action: addOrDeleteHealthWorkout) {
HStack { HStack {
Spacer() Spacer()
if isProcessingWorkout {
ProgressView()
.progressViewStyle(.circular)
.padding(.trailing, 10)
}
VStack { VStack {
Text("Matching workout found in Health") Text(healthButtonText)
.foregroundStyle(.black) .foregroundStyle(.accent)
if healthWorkout != nil {
Text("Tap to delete")
.font(.caption)
.foregroundStyle(.gray)
}
} }
Spacer() Spacer()
} }
.padding(.vertical, 8) .padding(.vertical, 8)
.listRowBackground(Color.accentColor)
} else {
Button(action: addWorkoutToHealth) {
HStack {
Spacer()
if isProcessingWorkout {
ProgressView()
.progressViewStyle(.circular)
.padding(.trailing, 10)
Text("Adding workout to health...")
.foregroundStyle(.accent)
} else {
Text("Add workout to health")
.foregroundStyle(.accent)
}
Spacer()
}
.padding(.vertical, 8)
}
.disabled(isProcessingWorkout)
} }
.disabled(isProcessingWorkout)
Section("Info") { Section("Info") {
DetailRow("Start", date: workout.startDate) DetailRow("Start", date: workout.startDate)
DetailRow("Duration", duration: workout.duration) DetailRow("Duration", duration: workout.duration)
@ -180,21 +174,65 @@ struct WorkoutDetailView: View {
} }
} }
private func addWorkoutToHealth() { private func updateButtonText() {
guard let db = database.store else { if isProcessingWorkout {
if healthWorkout == nil {
self.healthButtonText = "Adding workout to Health..."
} else {
self.healthButtonText = "Deleting workout from Health..."
}
} else {
if healthWorkout == nil {
healthButtonText = "Add workout to Health"
} else {
healthButtonText = "Found matching workout in Health"
}
}
}
private func addOrDeleteHealthWorkout() {
guard !isProcessingWorkout else {
return return
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.isProcessingWorkout = true self.isProcessingWorkout = true
updateButtonText()
} }
Task { Task {
await insert(workout: workout, using: db) if let healthWorkout {
await delete(healthWorkout: healthWorkout)
} else {
await addWorkoutToHealth()
}
DispatchQueue.main.async {
self.isProcessingWorkout = false
updateButtonText()
}
}
}
private func delete(healthWorkout: HKWorkout) async {
do {
try await store.store.delete(healthWorkout)
DispatchQueue.main.async {
self.healthWorkout = nil
self.isProcessingWorkout = false
}
} catch {
show("Failed to delete workout: \(error)")
DispatchQueue.main.async { DispatchQueue.main.async {
self.isProcessingWorkout = false self.isProcessingWorkout = false
} }
} }
} }
private func addWorkoutToHealth() async {
guard let db = database.store else {
return
}
await insert(workout: workout, using: db)
}
private func requestAuthorization() async -> Bool { private func requestAuthorization() async -> Bool {
do { do {
try await store.requestAuthorization( try await store.requestAuthorization(
@ -222,16 +260,28 @@ struct WorkoutDetailView: View {
show("No sharing permission for workout routes") show("No sharing permission for workout routes")
return return
} }
let samples: [HKSample] var samples: [HKSample]
do { do {
samples = try db.store.samples(associatedWith: workout) samples = try db.store.samples(associatedWith: workout)
} catch { } catch {
show("Failed to access samples associated with workout: \(error)") show("Failed to access samples associated with workout: \(error)")
return return
} }
// Add missing samples from statistics
let newSamples = makeStatisticSamples(workout, in: db)
for sample in newSamples {
guard !samples.contains(where: { $0.sampleType == sample.sampleType }) else {
continue
}
let typeName = HKQuantityTypeIdentifier(rawValue: sample.quantityType.identifier).description
print("Adding missing sample \(typeName)")
samples.append(sample)
}
let route: WorkoutRoute? let route: WorkoutRoute?
do { do {
route = try db.store.route(associatedWith: workout) route = try db.route(associatedWith: workout)
} catch { } catch {
show("Failed to get route associated with workout: \(error)") show("Failed to get route associated with workout: \(error)")
return return
@ -239,7 +289,7 @@ struct WorkoutDetailView: View {
let locations: [CLLocation] let locations: [CLLocation]
do { do {
locations = try route.map { locations = try route.map {
try db.store.locations(associatedWith: $0) try db.locations(associatedWith: $0)
} ?? [] } ?? []
} catch { } catch {
show("Failed to get locations associated with route: \(error)") show("Failed to get locations associated with route: \(error)")
@ -261,6 +311,22 @@ struct WorkoutDetailView: View {
} }
} }
private func makeStatisticSamples(_ workout: Workout, in db: HealthDatabase) -> [HKQuantitySample] {
let startDate = workout.startDate
let endDate = workout.endDate
let activity = workout.workoutActivities[0]
do {
let statistics = try db.statistics(associatedWith: activity)
return statistics.map {
.init(type: $0, quantity: $1.average, start: startDate, end: endDate)
}
} catch {
print("Failed to get statistics for activity: \(error)")
return []
}
}
private func loadSamples() { private func loadSamples() {
Task { Task {
await checkPermissionsAndSearchHealth() await checkPermissionsAndSearchHealth()
@ -318,11 +384,19 @@ struct WorkoutDetailView: View {
guard HKHealthStore.isHealthDataAvailable() else { guard HKHealthStore.isHealthDataAvailable() else {
return return
} }
DispatchQueue.main.async {
self.healthButtonText = "Checking for matching workout..."
self.isProcessingWorkout = true
}
do { do {
try await checkPermissionsAndFindWorkout() try await checkPermissionsAndFindWorkout()
} catch { } catch {
show("Failed to search for similar workout in Health: \(error)") show("Failed to search for similar workout in Health: \(error)")
} }
DispatchQueue.main.async {
self.isProcessingWorkout = false
updateButtonText()
}
} }
private func checkPermissionsAndFindWorkout() async throws { private func checkPermissionsAndFindWorkout() async throws {