iOS Apple HealthKit Apple Watch step data only - ios

Is there a way to get only Apple Watch step data from the HealthKit database. I have worked through examples in Swift 3 and have working code to obtain the merged data which Apple supplies but that contains step data from both the iPhone and Apple Watch.
I have not seen any examples where anyone was able to separate the two pieces of data. Every example I have seen only gives the merged step data. I can iterate over the step data sources but cannot use the source identifier to obtain the step data for a specific calendar time period.
All the Swift 3 code I have seen is along the following lines (Objective-C code follows similar functionality):
// Request the step count.
let stepType = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)
//let predicate = HKQuery.predicateForObjects(from: watchsource)
let predicate = HKQuery.predicateForSamples(withStart: startdate, end: enddate, options: [])
// Order doesn't matter, as we are receiving a quantity.
let sortDescriptor = NSSortDescriptor(key:HKSampleSortIdentifierEndDate, ascending:false)
// Fetch the steps.
let query = HKSampleQuery(sampleType:stepType!, predicate: predicate, limit: 0, sortDescriptors:[sortDescriptor]) { query, results, error in
var steps: Double = 0
if results != nil {
if results!.count > 0
{
for result in results as! [HKQuantitySample]
{
steps += result.quantity.doubleValue(for: HKUnit.count())
}
} // if results
} // if results != nil
completion(steps, error as NSError?)
} // HKSampleQuery()
healthKitStore.execute(query)
The above code as part of a proper HealthKit authentication works fine and I am able to obtain the step data for any time period.
However, there seems to be no way that HKSampleQuery() or another derivative HealthKit library call can be used for a particular source, ie, Apple Watch data only without the need for having an App on the Apple Watch itself to read the step data.
Yes, I know that one could read the iPhone pedometer data and then subtract that from the total number of steps but pedometer data is kept for only seven days, which is not much use if the time period is say, a month.
Has anyone solved this?

I was having the same issue. I wanted to get a separate step count for iPhone and Apple watch.
here is how I achieved this.
first, we need a predicate for devices to get only the Apple watch results.
let watchPredicate = HKQuery.predicateForObjects(withDeviceProperty: HKDevicePropertyKeyModel, allowedValues: ["Watch"])
now we can query for samples with this predicate and we'll just get the Apple watch entries.
we can also include more predicates to our query, using NSCompoundPredicate

Another approach is to fetch all the samples and then group step count values by the data source i.e. iPhone, Apple Watch, and third-party apps.
private lazy var store = HKHealthStore()
private func queryStepsCont(startDate: Date,
endDate: Date,
result: #escaping (Result<[(source: String, count: Int)], Error>) -> Void) {
guard let type = HKObjectType.quantityType(forIdentifier: .stepCount) else {
result(.failure(IntegrationsServiceError.preconditionFail))
return
}
let datePredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate)
let filteredByDateStepsCountQuery = HKSampleQuery(sampleType: type,
predicate: datePredicate,
limit: Int(HKObjectQueryNoLimit),
sortDescriptors: []) { _, samples, error in
if let error = error {
result(.failure(error))
return
}
let sourceNameStepsCount = Dictionary(grouping: samples ?? []) { sample in
sample.sourceRevision.source.name
}.map { sourceNameSamples -> (String, Int) in
let (sourceName, sourceSamples) = sourceNameSamples
let stepsCount = sourceSamples
.compactMap { ($0 as? HKQuantitySample)?.quantity.doubleValue(for: .count()) }
.reduce(0, +)
return (sourceName, Int(stepsCount))
}
result(.success(sourceNameStepsCount))
}
store.execute(filteredByDateStepsCountQuery)
}

Related

Swift Apple Health blood glucose

I got access to Apple Health and I'm able to read the glucose data which is in the Simulator.
guard let sampleType = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bloodGlucose) else {
fatalError("*** This method should never fail ***")
}
let query = HKSampleQuery(sampleType: sampleType, predicate: nil, limit: Int(HKObjectQueryNoLimit), sortDescriptors: nil) {
query, results, error in
guard let samples = results as? [HKQuantitySample] else {
// Handle any errors here.
return
}
for sample in samples {
print(sample)
}
I gives me this:
(2020-05-06 19:09:49 +0200 - 2020-05-06 19:09:49 +0200)
7.8 mmol<180.1558800000541>/L 811AACEB-F942-4A48-937B-568AD66E1BDE "Health" (13.3), "iPhone12,3" (13.3)metadata: {
HKWasUserEntered = 1; }
Is there any possibility to only print out the 7.8 mmol?
I didn't find anything in the documents from Apple. Thanks for the help.
sample is a class of type HKQuantitySample. If you print(sample) then it will print the complete class data.
If you want to print only quantity then try printing as below
print(sample.quantity)
I bet you would also need to extract the double value itself out of the quantitiy. Here is a sample code
let unit = HKUnit.gramUnit(with: .milli).unitDivided(by: HKUnit.liter())
let value = sample.quantity.doubleValue(for: unit)
For the source and device of the value you can try this:
let device = sample.device
let sourceRevision = sample.sourceRevision
If you want, you can try out my CocoaPod. It is a wrapper above HealthKit framework to ease the reading/writing operations. Here is the link: https://cocoapods.org/pods/HealthKitReporter

HealthKit Always Returns No Results when reading Blood Pressure

I have been trying various options to get results back, but no luck. I took the code from another question and tried to make it work, but still nothing. I manually entered the data into health as well as took readings from a BP monitor, so I know there is data there. No errors reported.
func readSampleByBloodPressure()
{
let past = Date.distantPast
let now = Date()
let sortDescriptor = NSSortDescriptor(key:HKSampleSortIdentifierStartDate, ascending: true)
let type = HKQuantityType.correlationType(forIdentifier: HKCorrelationTypeIdentifier.bloodPressure)
let sampleQuery = HKSampleQuery(sampleType: type!, predicate: nil, limit: 0, sortDescriptors: [sortDescriptor])
{ (sampleQuery, results, error ) -> Void in
let dataLst = results as? [HKCorrelation];
for data in dataLst!
{
let data1 = (data.objects(for: HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bloodPressureSystolic)!)).first as? HKQuantitySample
let data2 = data.objects(for: HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bloodPressureDiastolic)!).first as? HKQuantitySample
print("Data Found")
/*
if let value1 = data1!.quantity.doubleValue(for: HKUnit.millimeterOfMercury()) , let value2 = data2!.quantity.doubleValue(for: HKUnit.millimeterOfMercury())
{
print(value1)
print(value2)
}
*/
}
}
self.healthStore?.execute(sampleQuery)
}
}
This is caused by failing to authorize both systolic and diastolic blood pressure readings. In the authorization, when you get the popup from Health Kit, even though you select Authorize All, All implies only what is showing in the list presented, not everything HealthKit has available. In your auth code, you need to specify each thing you wish, which will then show up on the auth popup.
As a side note. If you have done this and if you check in the health app and see that what you want is authorized and it still isn't working, try toggling the setting. Apparently, the initial approval doesn't always take.

Delete health data that was previous stored from the same app in Swift?

Update Oct 7th
So after I read the answer, I now understand that I need to using query to retrive the data in Health and I try to using with codes:
let deletedType = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.dietaryCaffeine)
let predicate = HKQuery.predicateForSamples(withStart: dataDate as Date, end: dataDate as Date, options: .strictStartDate)
let findQuery = HKSampleQuery(sampleType: deletedType!, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) {
query, results, error in
if results != nil {
print("\nHere we got not nil on results!\n")
for result in (results as? [HKQuantitySample])! {
let quantity = result.quantity.doubleValue(for: HKUnit.gramUnit(with: .milli))
print(quantity)
}
} else {
print("results are nil")
return
}
}
healthKitStore.execute(findQuery)
I didn't do lot in the resultHander block, I firstly want to check what Data I found, and when I pring the quantity, I got noting, but I did get the "Here we got not nil on resluts" which means the results is not nil. I'm fresh to iOS developing and I check the document of HKHealthSample and cannot find which part of my HKSampleQuery wrong!
Original One:
I have an app that writes caffeine data into Health via HealthKit
Here is the save function
func saveCaffeine(_ caffeineRecorded: Double, dataDate: Date) {
// Set the quantity type to the running/walking distance.
let caffeineType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.dietaryCaffeine)
// Set the unit of measurement to miles.
let caffeineQuantity = HKQuantity(unit: HKUnit.gramUnit(with: .milli), doubleValue: caffeineRecorded)
// Set the official Quantity Sample.
let caffeine = HKQuantitySample(type: caffeineType!, quantity: caffeineQuantity, start: dataDate, end: dataDate)
print("\n to be added: \(caffeine) \n")
// Save the distance quantity sample to the HealthKit Store.
healthKitStore.save(caffeine, withCompletion: { (success, error) -> Void in
if( error != nil ) {
print(error!)
} else {
print("The Caffeine has been recorded! Better go check!")
}
})
}
Then It saved succeddfully, after that I retrive the data when I delete from the table view and pass to another delete function :
func deleteCaffeine(_ caffeineRecorded: Double, dataDate: Date){
let caffeineType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.dietaryCaffeine)
let caffeineQuantity = HKQuantity(unit: HKUnit.gramUnit(with: .milli), doubleValue: caffeineRecorded)
let coffeeBeenDeleted = HKQuantitySample(type: caffeineType!, quantity: caffeineQuantity, start: dataDate, end: dataDate)
print("\n to be deleted: \(coffeeBeenDeleted) \n")
healthKitStore.delete(coffeeBeenDeleted, withCompletion: {(success, error) -> Void in
if (error != nil) {
print(error!)
} else {
print("This caffeine data just been deleted!")
}
})
}
Then I got the error: Error Domain=com.apple.healthkit Code=3 "Failed to find some objects for deletion."
I using Realm to manage the database, I write the data into it that I can then retrieve it.
When I add it the HQuantitySample been printed is:
to be added: 30 mg (2017-10-06 18:36:25 -0400 - 2017-10-06 18:36:25 -0400)
When I delete the same one, the HQuantitySample been printed is: to be deleted: 30 mg (2017-10-06 18:36:25 -0400 - 2017-10-06 18:36:25 -0400)
As I understand, it should retrieve the same data since the amount and date is all right. Am I misunderstand anything about delete in HealthKit
You can't delete an HKQuantitySample that was previously saved by constructing a new HKQuantitySample that has similar properties. If there were two caffeine samples in HealthKit with the same start date, end date, and quantity which one would you expect HealthKit to delete? Each HKObject is unique and to delete an object you must first find it using a query and then pass the object you got from the query to delete().
You need to define your specific key in HKMetadataKeySyncIdentifier before you save your data to apple health. And then, you can use HKMetadataKeySyncIdentifier to delete specific health data.
You can try my answer:
https://stackoverflow.com/a/69624769/8094919

Adding HealthKit Data into one value

I am using HealthKit to make a sample query data such as the step count. However, when I test it on my device I get a bunch of different results. Now since I have the results from different sources and different days such as [16 count, 50 count, .....]. Now I want to add up all of the data into one value. How would I achieve this? For example if I make a sample query to HealthKit, and it returns [15 count, 20 count] I want to 15 + 20 to get 35 count. How would I do that?
Here is the code that I used to query the data:
func getStepsHealthData() {
let stepsHealthDataQuery = HKSampleQuery(sampleType: stepsHealth, predicate: predicate, limit: Int(HKObjectQueryNoLimit), sortDescriptors: nil) {
(query, results, error) in
let stepsUnit = HKUnit.countUnit()
for result in (results as? [HKQuantitySample])! {
stepCount = result.quantity.doubleValueForUnit(stepsUnit)
}
}
healthKitStore?.executeQuery(stepsHealthDataQuery)
}
You can definitely do what you want with a HKSampleQuery, you just need to keep a totalSum variable and iterate over every value.
That said there is an specific query type just for the kind of thing that you want to do called HKStatisticsQuery. According to docs:
Statistics queries perform statistical calculations over the set of
matching quantity samples
A getTotalSteps function could be done this way:
func getTotalSteps() {
let stepsType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)!
let stepsUnit = HKUnit.countUnit()
let sumOption = HKStatisticsOptions.CumulativeSum
let statisticsSumQuery = HKStatisticsQuery(quantityType: stepsType, quantitySamplePredicate: nil,
options: sumOption)
{ (query, result, error) in
if let sumQuantity = result?.sumQuantity() {
let numberOfSteps = Int(sumQuantity.doubleValueForUnit(stepsUnit))
print(numberOfSteps)
}
}
healthStore.executeQuery(statisticsSumQuery)
}
HKStatisticsOptions.CumulativeSum does the trick and the remaining code isn't too different from what you know about HKSampleQuery.
Check it out the docs for further reading and also take a look at the HKStatistics class which provides more options to perform statistical calculations like the previous one.

hkworkout totalenergyburned doesn't correlate with HKQuantityTypeIdentifierActiveEnergyBurned

This is more of a data structure question than a programming syntax question. The data structure of the Health app is somewhat of a black box.
I want to query HKHealthStore and create both a daily summary of items including, ActiveEnergyBurned, and also a summary of workouts including totalEnergyBurned.
I have code (below) that successfully retrieves this information. However the number for the daily total is usually LESS than that day's workout! I am sure that my code is not the problem, because the exact same numbers show up in the Apple Health app. For example:
yesterday's workout:
My app
workout.totalEnergyBurned = 905 kcal
sum of yesterday's ActiveEnergyBurned 655 kcal
Health app shows the exact same numbers for both.
If ActiveEnergyBurned, doesn't include workouts, what does it include? I didn't have another 655 of ActiveEnergyBurned. It doesn't seem possible to me that ActiveEnergyBurned wouldn't include workouts!
//to get sum of day's activeCaloriesBurned:
func getActiveCalories(startDate:NSDate, endDate:NSDate){
let sampleType = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierActiveEnergyBurned)
let hkUnit = HKUnit.kilocalorieUnit()
getSumStatsFor(sampleType, hkUnit: hkUnit, startDate: startDate, endDate: endDate) { (hdObject, result) -> Void in
hdObject.activeCalories = result
}
}
func getTotalsForDataType(quantitiyType:HKQuantityType, startDate:NSDate, endDate:NSDate, completion:(HKStatisticsCollection!, NSError!) -> Void){
println("getTotalsForDataType start: \(startDate) end: \(endDate)")
let dayStart = NSCalendar.currentCalendar().startOfDayForDate(startDate)
let addDay = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: endDate, options:nil)
let dayEnd = NSCalendar.currentCalendar().startOfDayForDate(addDay!) //add one day
let interval = NSDateComponents()
interval.day = 1
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: HKQueryOptions.None)
let newQuery = HKStatisticsCollectionQuery(quantityType: quantitiyType,
quantitySamplePredicate: predicate,
options: HKStatisticsOptions.CumulativeSum,
anchorDate: dayStart,
intervalComponents: interval)
newQuery.initialResultsHandler = {
query, results, error in
if error != nil {
println("*** An error occurred while calculating the statistics: \(error.localizedDescription) ***")
completion(nil, error)
}else{
completion(results,error)
}
}
self.healthKitStore.executeQuery(newQuery)
}
//to get workout totalCalories burned
func readWorkouts(completion: (([AnyObject]!, NSError!) ->Void)!){
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
let sampleQuery = HKSampleQuery(sampleType: HKWorkoutType.workoutType(), predicate: nil, limit: 0, sortDescriptors: [sortDescriptor]) { (sampleQuery, results, error) -> Void in
if let queryError = error {
println( "There was an error while reading the samples: \(queryError.localizedDescription)")
}
completion(results,error)
}
healthKitStore.executeQuery(sampleQuery)
}
This is due to a bug with how the HealthKit workout API was designed. With the current API it's possible for 3rd party apps to create HKWorkouts where totalEnergyBurned is greater than 0, but no associated HKSamples of the HKQuantityTypeIdentifierActiveEnergyBurned type are created to sum up to totalEnergyBurned. As an example, a 3rd party app that feeds workout data into HealthKit could do this:
HKHealthStore *healthStore = [HKHealthStore new];
HKWorkout *workout = [HKWorkout workoutWithActivityType:HKWorkoutActivityTypePlay
startDate:[NSDate date]
endDate:[NSDate date]
duration:100.0
totalEnergyBurned:[HKQuantity quantityWithUnit:[HKUnit kilocalorieUnit] doubleValue:445]
totalDistance:[HKQuantity quantityWithUnit:[HKUnit meterUnit] doubleValue:1000]
metadata:nil];
[healthStore saveObject:workout withCompletion:nil];
Note that nowhere are any HKSamples created of type HKQuantityTypeIdentifierActiveEnergyBurned. Then when you sum up the active energy burned for the day and compare it to the workout's total energy burned you will get 0 kcal vs 445 kcal. What a good 3rd party app would do is this after creating the workout:
NSArray *workoutSamples = #[[HKQuantitySample quantitySampleWithType:HKQuantityTypeIdentifierActiveEnergyBurned
quantity:[HKQuantity quantityWithUnit:[HKUnit kilocalorieUnit] doubleValue:workout.totalEnergyBurned]
startDate:workout.startDate
endDate:workout.endDate]];
[healthStore addSamples:workoutSamples toWorkout:workout completion:nil];
That way there's at least only active energy burned sample. Now you'll get 445 kcal vs 445 kcal.
In my tests with 3rd party apps, I've found that most do add the active energy burned samples, but some, like Nike Running, do not.
A hacky workaround for this would be to pull out all of the active energy samples for a workout (you'll have to use startDate and endDate because predicateForObjectsFromWorkout has similar issues as noted above), then if there aren't any samples, assume that the source did not create active energy samples for that workout and add the workout's totalEnergyBurned to the active energy burned sum for the day.

Resources