diff --git a/HealthImport/WorkoutDetailView.swift b/HealthImport/WorkoutDetailView.swift index 31a33df..26f7450 100644 --- a/HealthImport/WorkoutDetailView.swift +++ b/HealthImport/WorkoutDetailView.swift @@ -40,6 +40,9 @@ struct WorkoutDetailView: View { @State private var isProcessingWorkout = false + @State + private var healthButtonText = "Checking for workout in Health..." + private var metadataFields: [(key: String, value: Any)] { workout.metadata.sorted { $0.key } } @@ -51,37 +54,28 @@ struct WorkoutDetailView: View { var body: some View { List { - if healthWorkout != nil { + Button(action: addOrDeleteHealthWorkout) { HStack { Spacer() + if isProcessingWorkout { + ProgressView() + .progressViewStyle(.circular) + .padding(.trailing, 10) + } VStack { - Text("Matching workout found in Health") - .foregroundStyle(.black) + Text(healthButtonText) + .foregroundStyle(.accent) + if healthWorkout != nil { + Text("Tap to delete") + .font(.caption) + .foregroundStyle(.gray) + } } Spacer() } .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") { DetailRow("Start", date: workout.startDate) DetailRow("Duration", duration: workout.duration) @@ -180,21 +174,65 @@ struct WorkoutDetailView: View { } } - private func addWorkoutToHealth() { - guard let db = database.store else { + private func updateButtonText() { + 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 } DispatchQueue.main.async { self.isProcessingWorkout = true + updateButtonText() } 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 { 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 { do { try await store.requestAuthorization( @@ -222,16 +260,28 @@ struct WorkoutDetailView: View { show("No sharing permission for workout routes") return } - let samples: [HKSample] + var samples: [HKSample] do { samples = try db.store.samples(associatedWith: workout) } catch { show("Failed to access samples associated with workout: \(error)") 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? do { - route = try db.store.route(associatedWith: workout) + route = try db.route(associatedWith: workout) } catch { show("Failed to get route associated with workout: \(error)") return @@ -239,7 +289,7 @@ struct WorkoutDetailView: View { let locations: [CLLocation] do { locations = try route.map { - try db.store.locations(associatedWith: $0) + try db.locations(associatedWith: $0) } ?? [] } catch { 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() { Task { await checkPermissionsAndSearchHealth() @@ -318,11 +384,19 @@ struct WorkoutDetailView: View { guard HKHealthStore.isHealthDataAvailable() else { return } + DispatchQueue.main.async { + self.healthButtonText = "Checking for matching workout..." + self.isProcessingWorkout = true + } do { try await checkPermissionsAndFindWorkout() } catch { show("Failed to search for similar workout in Health: \(error)") } + DispatchQueue.main.async { + self.isProcessingWorkout = false + updateButtonText() + } } private func checkPermissionsAndFindWorkout() async throws {