From 3e63adf363363669a7f14293f35ac645636b0c5c Mon Sep 17 00:00:00 2001 From: rzen Date: Sun, 5 Jul 2026 07:55:03 -0400 Subject: [PATCH] Offer to also remove a deleted workout from Apple Health Closes the last open loop of the HealthKit feature (decision C from HealthKit-Delete-Loop.md, now retired): the history-list delete dialog gains a "Delete + Remove from Apple Health" option, shown only when the workout has a Health record. Removal is best-effort via the workout's stored HKWorkout UUID; watch-recorded workouts are authored by the watch app's separate HealthKit source and may be refused, so the dialog notes they may need deleting in the Health app instead. Discarding an in-progress workout is unaffected (no Health record exists yet). --- CHANGELOG.md | 2 ++ Workouts/HealthKit/WorkoutHealthWriter.swift | 19 +++++++++++++++++++ .../Views/WorkoutLogs/WorkoutLogsView.swift | 14 ++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1210045..a9f06b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ **July 2026** +When deleting a workout that was saved to Apple Health, you can now choose to remove it from Apple Health too. Workouts recorded by Apple Watch may need to be deleted in the Health app instead. + Sync problems are now shown in Settings instead of failing silently. Fixed workouts and splits vanishing from the app when iPhone storage ran low: when iOS offloads their files to iCloud to free up space, Workouts now downloads them back automatically instead of treating them as gone. diff --git a/Workouts/HealthKit/WorkoutHealthWriter.swift b/Workouts/HealthKit/WorkoutHealthWriter.swift index b758568..3f1c3af 100644 --- a/Workouts/HealthKit/WorkoutHealthWriter.swift +++ b/Workouts/HealthKit/WorkoutHealthWriter.swift @@ -83,6 +83,25 @@ final class WorkoutHealthWriter { pending[id] = nil } + /// Best-effort removal of a workout's Health record, on the user's request when + /// deleting the workout in the app. HealthKit only allows deleting samples this + /// app authored, so phone estimates delete cleanly while watch-recorded workouts + /// (authored by the watch app's separate HealthKit source) may be refused — that + /// refusal is swallowed; the record then stays in Health until removed there. + func deleteFromHealth(uuidString: String) { + guard HKHealthStore.isHealthDataAvailable(), let uuid = UUID(uuidString: uuidString) else { return } + let healthStore = self.healthStore + let query = HKSampleQuery( + sampleType: .workoutType(), + predicate: HKQuery.predicateForObject(with: uuid), + limit: 1, sortDescriptors: nil + ) { _, samples, _ in + guard let workout = samples?.first else { return } + healthStore.delete([workout]) { _, _ in } + } + healthStore.execute(query) + } + /// Launch safety sweep: catch workouts that completed but never got a Health record /// (e.g., the app was killed during the debounce). Bounded to the last day so we /// don't retroactively flood Health with historical workouts; idempotent via the diff --git a/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift b/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift index 75c096a..2fd608b 100644 --- a/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift +++ b/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift @@ -12,6 +12,7 @@ import SwiftData struct WorkoutLogsView: View { @Environment(SyncEngine.self) private var sync + @Environment(AppServices.self) private var services @Query(sort: \Workout.start, order: .reverse) private var workouts: [Workout] @@ -88,9 +89,22 @@ struct WorkoutLogsView: View { Task { await sync.delete(workout: workout) } itemToDelete = nil } + // Only offered when the workout actually has a Health record. Capture + // the UUID before the delete prunes the cache entity. + if let healthUUID = workout.metricHealthKitWorkoutUUID { + Button("Delete + Remove from Apple Health", role: .destructive) { + services.workoutHealthWriter.deleteFromHealth(uuidString: healthUUID) + Task { await sync.delete(workout: workout) } + itemToDelete = nil + } + } Button("Cancel", role: .cancel) { itemToDelete = nil } + } message: { workout in + if workout.metricSourceRaw == MetricSource.watch.rawValue { + Text("This workout was recorded by Apple Watch; if it can't be removed from Apple Health here, delete it in the Health app.") + } } } }