The phone no longer writes estimated Health workouts: the watch, which runs the live session, is the sole recorder. Replaces WorkoutHealthWriter with a WorkoutHealthDeleter that only removes a legacy phone-estimate workout when its record is deleted here, drops the MET calorie table and the phone's write/read Health scopes, and keeps phoneEstimate decodable for existing documents. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
79 lines
3.3 KiB
Swift
79 lines
3.3 KiB
Swift
//
|
|
// WorkoutHealthDeleter.swift
|
|
// Workouts
|
|
//
|
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import HealthKit
|
|
import os
|
|
|
|
/// Removes a workout's Apple Health record when the user deletes that workout in the app.
|
|
///
|
|
/// Apple Health recording is **watch-only**: the watch's `HKWorkoutSession`
|
|
/// (`WorkoutSessionManager`) is the sole recorder, and the phone writes nothing to
|
|
/// Health. This helper exists purely so that *legacy* phone-estimate workouts — the
|
|
/// MET-based `HKWorkout`s that app versions up to 2.x wrote from the phone — can still
|
|
/// be pruned from Health when the user deletes them here.
|
|
///
|
|
/// HealthKit only permits deleting samples this app authored, so those phone estimates
|
|
/// delete cleanly while watch-recorded workouts (authored by the watch app's separate
|
|
/// HealthKit source) are refused — the delete flow directs the user to the Health app.
|
|
@MainActor
|
|
final class WorkoutHealthDeleter {
|
|
private let healthStore = HKHealthStore()
|
|
private let log = Logger(subsystem: "dev.rzen.indie.Workouts", category: "health")
|
|
|
|
private var isScreenshotRun: Bool {
|
|
#if DEBUG
|
|
return ScreenshotSeed.isActive
|
|
#else
|
|
return false
|
|
#endif
|
|
}
|
|
|
|
/// Request the single scope a delete needs: share (write) access to the workout
|
|
/// type. HealthKit requires write authorization for a sample's type in order to
|
|
/// delete a sample this app authored. No read scopes — the phone reads nothing
|
|
/// from Health (heart rate and calories come from the watch over WatchConnectivity).
|
|
func authorizeIfNeeded() {
|
|
guard HKHealthStore.isHealthDataAvailable(), !isScreenshotRun else { return }
|
|
healthStore.requestAuthorization(
|
|
toShare: WorkoutHealthAuthorization.phoneShare,
|
|
read: []
|
|
) { _, _ in }
|
|
}
|
|
|
|
/// Best-effort removal of a workout's Health record, on the user's request when
|
|
/// deleting the workout in the app. Only samples this app authored (legacy phone
|
|
/// estimates) can be deleted; watch-recorded workouts are refused by HealthKit and
|
|
/// remain in Health until removed there.
|
|
func deleteFromHealth(uuidString: String) {
|
|
guard HKHealthStore.isHealthDataAvailable(), let uuid = UUID(uuidString: uuidString) else { return }
|
|
guard healthStore.authorizationStatus(for: .workoutType()) == .sharingAuthorized else {
|
|
log.notice("Skipping Health delete: workout sharing not authorized")
|
|
return
|
|
}
|
|
let healthStore = self.healthStore
|
|
let log = self.log
|
|
let query = HKSampleQuery(
|
|
sampleType: .workoutType(),
|
|
predicate: HKQuery.predicateForObject(with: uuid),
|
|
limit: 1, sortDescriptors: nil
|
|
) { _, samples, error in
|
|
if let error {
|
|
log.error("Health workout lookup failed: \(error.localizedDescription, privacy: .public)")
|
|
return
|
|
}
|
|
guard let workout = samples?.first else { return }
|
|
healthStore.delete([workout]) { _, error in
|
|
if let error {
|
|
log.error("Health workout delete failed: \(error.localizedDescription, privacy: .public)")
|
|
}
|
|
}
|
|
}
|
|
healthStore.execute(query)
|
|
}
|
|
}
|