I tried to use HealthKit to fetch my own running data in Swift.
For this I created a HealthManager class for requesting authorization to reading "Workouts" data from HealthKit.
For requesting the authorization I added "Privacy - Health Share Usage Description" key in Info.plist
After this I tried to fetch my running data with this function :
func fetchRunningData() {
// Closure for convert meters to kilometers
let convertToKilometers = { $0 / 1000.0 }
let calendar = Calendar.current
let endDate = Date() // End date is today
let startDate = calendar.date(byAdding: .day, value: -30, to: endDate)!
// Weekly stats
var weeklyDistance = 0.0
var weeklyElevation = 0.0
var weeklyDuration = 0.0
// Filters
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
HKQuery.predicateForSamples(withStart: startDate, end: endDate),
HKQuery.predicateForWorkouts(with: .running)
])
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
let query = HKSampleQuery(sampleType: .workoutType(),
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: [sortDescriptor]) { (query, results, error) in
if let workouts = results as? [HKWorkout], error == nil {
// Process the retrieved workouts
for workout in workouts {
let distance = workout.totalDistance?.doubleValue(for: .meter()) ?? 0.0
let duration = workout.duration
// fetch elevation
guard let workoutMetadata = workout.metadata,
let workoutElevation = workoutMetadata["HKElevationAscended"] as? HKQuantity else {
return
}
let elevation = workoutElevation.doubleValue(for: .meter())
weeklyDistance += convertToKilometers(distance)
weeklyElevation += convertToKilometers(elevation)
weeklyDuration += duration
}
print("Weekly statistics -> Distance: \(weeklyDistance) - Elevation: \(weeklyElevation) - Duration: \(weeklyDuration)")
} else {
// Handle the case where the query fails or there's an error
if let error = error {
print("Error retrieving running data: \(error.localizedDescription)")
}
}
}
healthStore.execute(query)
}
When I run this code (on a real device) app requesting access for workouts data, I allow this and the app start, but my running data are never fetched. The print into my function is never executed.
The query is correct and access is granted because the print into the 'else' is not executed.
That weird because my app works as if there is no running data into my HealthKit data but on my iPhone I have many running workouts for the last 30 days.
Here my entire code.
import Foundation
import HealthKit
class HealthManager: ObservableObject {
let healthStore = HKHealthStore()
init() {
let readTypes = HKObjectType.workoutType()
let healthTypes: Set = [readTypes]
// Request user permission for access to his health data
// We need to add a key in Info.plist for request permission
Task {
do {
try await healthStore.requestAuthorization(toShare: [], read: healthTypes)
} catch {
print("error fetching health data")
}
}
}
func fetchRunningData() {
// Closure for convert meters to kilometers
let convertToKilometers = { $0 / 1000.0 }
let calendar = Calendar.current
let endDate = Date() // End date is today
let startDate = calendar.date(byAdding: .day, value: -30, to: endDate)!
// Weekly stats
var weeklyDistance = 0.0
var weeklyElevation = 0.0
var weeklyDuration = 0.0
// Filters
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
HKQuery.predicateForSamples(withStart: startDate, end: endDate),
HKQuery.predicateForWorkouts(with: .cycling)
])
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
let query = HKSampleQuery(sampleType: .workoutType(),
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: [sortDescriptor]) { (query, results, error) in
if let workouts = results as? [HKWorkout], error == nil {
// Process the retrieved workouts
for workout in workouts {
let distance = workout.totalDistance?.doubleValue(for: .meter()) ?? 0.0
let duration = workout.duration
// fetch elevation
guard let workoutMetadata = workout.metadata,
let workoutElevation = workoutMetadata["HKElevationAscended"] as? HKQuantity else {
return
}
let elevation = workoutElevation.doubleValue(for: .meter())
weeklyDistance += convertToKilometers(distance)
weeklyElevation += convertToKilometers(elevation)
weeklyDuration += duration
}
print("Weekly statistics -> Distance: \(weeklyDistance) - Elevation: \(weeklyElevation) - Duration: \(weeklyDuration)")
} else {
// Handle the case where the query fails or there's an error
if let error = error {
print("Error retrieving running data: \(error.localizedDescription)")
}
}
}
healthStore.execute(query)
}
}
I tried to change the workout type in my predicate.
I replace .running by .cycling and when I run the app my cycling data are fetched and printed in the log.
Does anyone understand why the running data are no fetched ?
UPDATE
When I try to print my stats in the for loop the data are fetched but I can't use them after the loop. But that's so confusing because my variables are declared in the beginning of the function and if I replace running by cycling data are available in and out the loop.
let query = HKSampleQuery(sampleType: .workoutType(),
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: [sortDescriptor]) { (query, results, error) in
if let workouts = results as? [HKWorkout], error == nil {
// Process the retrieved workouts
for workout in workouts {
let distance = workout.totalDistance?.doubleValue(for: .meter()) ?? 0.0
let duration = workout.duration
// fetch elevation
guard let workoutMetadata = workout.metadata,
let workoutElevation = workoutMetadata["HKElevationAscended"] as? HKQuantity else {
return
}
let elevation = workoutElevation.doubleValue(for: .meter())
weeklyDistance += convertToKilometers(distance)
weeklyElevation += convertToKilometers(elevation)
weeklyDuration += duration
print(weeklyDistance) // Displayed if workout is running or cycling
}
print(weeklyDistance) // displayed on if workout is cycling
//print("Weekly statistics -> Distance: \(weeklyDistance) - Elevation: \(weeklyElevation) - Duration: \(weeklyDuration)")
} else {
// Handle the case where the query fails or there's an error
if let error = error {
print("Error retrieving running data: \(error.localizedDescription)")
}
}