We looked previously at the basic HKSampleQuery and the HKAnchoredObjectQuery. Both provided ways to query samples in HealthKit and work with data that the user had provided permission for.
The next query we will look at is the HKStatisticsQuery. This query is particularly interesting to those who want to calculate sum, minimum, maximum, or average values.
Lets begin by downloading the project from where we left off last time.
Creating a HKStatisticsQuery
We will be using step count for these queries for a couple of reasons. First, our starter project already requests this permission from the user, and second, step count is a quantity type and many of you will have step data to query on your iPhone.
func testStatisticsQuery() {
guard let stepCountType = HKObjectType.quantityType(forIdentifier: .stepCount) else {
fatalError("*** Unable to get the step count type ***")
}
let query = HKStatisticsQuery.init(quantityType: stepCountType,
quantitySamplePredicate: nil,
options: HKStatisticsOptions.cumulativeSum) { (query, results, error) in
print("Total: \(results?.sumQuantity()?.doubleValue(for: HKUnit.count()))")
}
healthStore.execute(query)
}
Lines 2 – 4 we prepare our quantity type.
Line 6 we initialise the HKStatisticsQuery and pass in the quantity type. On the next line we specify that we are using no predicate. This will request all data.
Line 8 is where we set our options. It asks for a list of HKStatisticsOptions. Looking at the class documentation we see that we have various options available, some of which we can combine if needed. What you need to look out for is getting the correct options for the correct type of data. Some options are cumulative while others are discrete. You cannot mix these up. To find out which types are cumulative values and which are discrete values you can check on this page.
On that page you can search for the quantity type you are querying, click on the link for the sample, and in the discussion section it will mention what type it is. If you click on stepCount we can see it is a cumulative value, whereas bodyMass contains discreet values.
For stepCount we have just 2 options. One is the cumulativeSum of samples between the dates provided in the predicate (or all samples if the predicate is nil). The other is an option to separate by source. The source refers to which app or device was used to create the samples. In the example above we do not separate by source. In a few moments we will look at that option.
Line 12 is where we execute the query, and line 9 is what runs when the completion handler is called. You might notice on line 9 that we have a bit of extra syntax than you might expect. The sumQuantity property returns a HKQuantity. When we look at this, we can call the doubleValue(for:) instance method, but this requires a HKUnit. When we look at HKUnit we find that .count is the most appropriate for this type of data.
If you run the app now, you should get a total step count since the first step count recorded in Health.
seperateBySource
Next we will look at what happens when we select the seperateBySource option.
func testStatisticsQuery() {
guard let stepCountType = HKObjectType.quantityType(forIdentifier: .stepCount) else {
fatalError("*** Unable to get the step count type ***")
}
let query = HKStatisticsQuery.init(quantityType: stepCountType,
quantitySamplePredicate: nil,
options: [HKStatisticsOptions.cumulativeSum, HKStatisticsOptions.separateBySource]) { (query, results, error) in
print("Total: \(results?.sumQuantity()?.doubleValue(for: HKUnit.count()))")
for source in (results?.sources)! {
print("Seperate Source: \(results?.sumQuantity(for: source)?.doubleValue(for: HKUnit.count()))")
}
}
healthStore.execute(query)
}
In the sample code above we have adjusted line 8. We pass in an array containing the cumulativeSum and seperateBySource options. [HKStatisticsOptions.cumulativeSum, HKStatisticsOptions.separateBySource].
On line 9 we print the sumQuantity of results. This is the same as the previous example which is the cumulative sum of everything converted to a double with the HKUnit count property.
On lines 10, 11, 12 we now have an array of sources to work with in results. If you did not specify seperateBySource this array would contain nil values for the quantity sum of each source. What we do here is iterate through each source, and then we print the sumQuantity but use the sumQuantity(for:) instance method. We pass in the source, and then each source has it’s quantityValue logged to the console, although just like in previous examples, we convert to a double.
You might notice that when this particular version of the code runs, the total of the individual sources is greater than the cumulativeSum of all combined (line 9). I believe this is because Apple Health contains duplicate records for step counts. When you have an Apple Watch and iPhone counting steps, there appears to be some cleanup for when steps are counted at the same time. When the query runs for separate sources, it seems to give the full amount of steps for each, but not cleaned. Just be aware of that in case you mis-represent step counts.
HKStatisticsQuery for Body Mass
I mentioned earlier that step count is cumulative. We also learned that body mass is discrete. For discrete samples we have a few different options. In the example below we will look at the discreteAverage with the seperateBySource option added.
func testStatisticsQueryDiscrete() {
guard let bodyMassType = HKObjectType.quantityType(forIdentifier: .bodyMass) else {
fatalError("*** Unable to get the body mass type ***")
}
let query = HKStatisticsQuery.init(quantityType: bodyMassType,
quantitySamplePredicate: nil,
options: [HKStatisticsOptions.discreteAverage, HKStatisticsOptions.separateBySource]) { (query, results, error) in
print("Total: \(results?.averageQuantity()?.doubleValue(for: HKUnit.pound()))")
for source in (results?.sources)! {
print("Seperate Source: \(results?.averageQuantity(for: source)?.doubleValue(for: HKUnit.pound()))")
}
}
healthStore.execute(query)
}
Line 2 we change from stepCount to bodyMass. We have already sought permission from the user for this, so can easily make the change just here.
Line 8 is changed to specify the .discreteAverage.
Line 9 prints the averageQuantity converted to double by HKUnit.pound(). You can change this to your preferred quantity type for body weight.
Line 10 – 12 works through each source and prints off the average for each source of data.
You can make changes to the options and print statements to get other discrete calculations such as changing it to discreteMax as follows:
print("Total: \(results?.maximumQuantity()?.doubleValue(for: HKUnit.pound()))")
for source in (results?.sources)! {
print("Seperate Source: \(results?.maximumQuantity()(for: source)?.doubleValue(for: HKUnit.pound()))")
}
This will print out the highest recorded quantity you recorded for bodyMass in Apple Health.
Closing
The HKStatisticsQuery is another tool that you might use when wanting to perform quick calculations on quantity samples you are interested in. For bodyMass, having the max and min values readily available with a query makes it possible for you to create a chart that can scale to the correct values. There are many uses from being able to quickly gather statistics. I didn’t mention all options here. Others include discreteMin and discreteMostRecent which might be helpful for you to quickly get the last quantity for a given type.
The project can be downloaded here, and the full source for ViewController.swift is below.
import UIKit
import HealthKit
class ViewController: UIViewController {
let healthStore = HKHealthStore()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if HKHealthStore.isHealthDataAvailable() {
let readDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!,
HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.biologicalSex)!,
HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.dateOfBirth)!,
HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bodyMass)!]
let writeDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!]
healthStore.requestAuthorization(toShare: writeDataTypes, read: readDataTypes) { (success, error) in
if !success {
// Handle the error here.
} else {
//self.testCharachteristic()
//self.testSampleQuery()
//self.testSampleQueryWithPredicate()
//self.testSampleQueryWithSortDescriptor()
//self.testAnchoredQuery()
self.testStatisticsQueryCumulitive()
self.testStatisticsQueryDiscrete()
}
}
}
}
// Fetches biologicalSex of the user, and date of birth.
func testCharachteristic() {
// Biological Sex
if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.female {
print("You are female")
} else if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.male {
print("You are male")
} else if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.other {
print("You are not categorised as male or female")
}
// Date of Birth
if #available(iOS 10.0, *) {
try! print(healthStore.dateOfBirthComponents())
} else {
// Fallback on earlier versions
do {
let dateOfBirth = try healthStore.dateOfBirth()
print(dateOfBirth)
} catch let error {
print("There was a problem fetching your data: \(error)")
}
}
}
// HKSampleQuery with a nil predicate
func testSampleQuery() {
// Simple Step count query with no predicate and no sort descriptors
let sampleType = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)
let query = HKSampleQuery.init(sampleType: sampleType!,
predicate: nil,
limit: HKObjectQueryNoLimit,
sortDescriptors: nil) { (query, results, error) in
print(results)
}
healthStore.execute(query)
}
// HKSampleQuery with a predicate
func testSampleQueryWithPredicate() {
let sampleType = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bodyMass)
let today = Date()
let startDate = Calendar.current.date(byAdding: .month, value: -3, to: today)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: today, options: HKQueryOptions.strictEndDate)
let query = HKSampleQuery.init(sampleType: sampleType!,
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: nil) { (query, results, error) in
print(results)
}
healthStore.execute(query)
}
// Sample query with a sort descriptor
func testSampleQueryWithSortDescriptor() {
let sampleType = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bodyMass)
let today = Date()
let startDate = Calendar.current.date(byAdding: .month, value: -3, to: today)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: today, options: HKQueryOptions.strictEndDate)
let query = HKSampleQuery.init(sampleType: sampleType!,
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)]) { (query, results, error) in
print(results)
}
healthStore.execute(query)
}
func testAnchoredQuery() {
guard let bodyMassType = HKObjectType.quantityType(forIdentifier: .bodyMass) else {
fatalError("*** Unable to get the body mass type ***")
}
var anchor = HKQueryAnchor.init(fromValue: 0)
if UserDefaults.standard.object(forKey: "Anchor") != nil {
let data = UserDefaults.standard.object(forKey: "Anchor") as! Data
anchor = NSKeyedUnarchiver.unarchiveObject(with: data) as! HKQueryAnchor
}
let query = HKAnchoredObjectQuery(type: bodyMassType,
predicate: nil,
anchor: anchor,
limit: HKObjectQueryNoLimit) { (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in
guard let samples = samplesOrNil, let deletedObjects = deletedObjectsOrNil else {
fatalError("*** An error occurred during the initial query: \(errorOrNil!.localizedDescription) ***")
}
anchor = newAnchor!
let data : Data = NSKeyedArchiver.archivedData(withRootObject: newAnchor as Any)
UserDefaults.standard.set(data, forKey: "Anchor")
for bodyMassSample in samples {
print("Samples: \(bodyMassSample)")
}
for deletedBodyMassSample in deletedObjects {
print("deleted: \(deletedBodyMassSample)")
}
print("Anchor: \(anchor)")
}
query.updateHandler = { (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in
guard let samples = samplesOrNil, let deletedObjects = deletedObjectsOrNil else {
// Handle the error here.
fatalError("*** An error occurred during an update: \(errorOrNil!.localizedDescription) ***")
}
anchor = newAnchor!
let data : Data = NSKeyedArchiver.archivedData(withRootObject: newAnchor as Any)
UserDefaults.standard.set(data, forKey: "Anchor")
for bodyMassSample in samples {
print("samples: \(bodyMassSample)")
}
for deletedBodyMassSample in deletedObjects {
print("deleted: \(deletedBodyMassSample)")
}
}
healthStore.execute(query)
}
func testStatisticsQueryCumulitive() {
guard let stepCountType = HKObjectType.quantityType(forIdentifier: .stepCount) else {
fatalError("*** Unable to get the step count type ***")
}
let query = HKStatisticsQuery.init(quantityType: stepCountType,
quantitySamplePredicate: nil,
options: [HKStatisticsOptions.cumulativeSum, HKStatisticsOptions.separateBySource]) { (query, results, error) in
print("Total: \(results?.sumQuantity()?.doubleValue(for: HKUnit.count()))")
for source in (results?.sources)! {
print("Seperate Source: \(results?.sumQuantity(for: source)?.doubleValue(for: HKUnit.count()))")
}
}
healthStore.execute(query)
}
func testStatisticsQueryDiscrete() {
guard let bodyMassType = HKObjectType.quantityType(forIdentifier: .bodyMass) else {
fatalError("*** Unable to get the body mass type ***")
}
let query = HKStatisticsQuery.init(quantityType: bodyMassType,
quantitySamplePredicate: nil,
options: [HKStatisticsOptions.discreteMax, HKStatisticsOptions.separateBySource]) { (query, results, error) in
print("Total: \(results?.maximumQuantity()?.doubleValue(for: HKUnit.pound()))")
for source in (results?.sources)! {
print("Seperate Source: \(results?.maximumQuantity(for: source)?.doubleValue(for: HKUnit.pound()))")
}
}
healthStore.execute(query)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Leave a Reply
You must be logged in to post a comment.