In the last tutorial we covered reading characteristic data from HealthKit. Characteristic data is read by accessing direct methods on the health store. These queries are simple because items like date of birth and biological sex do not typically change. However, there are many data types stored in Apple Health that do change such as step counts, heart rate, body mass, and so on. For these types of data Apple allows us to create queries so that we can fetch the data we want.
Many of your requests to HealthKit will be done by using a query. Queries allow you to fetch a snapshot of data from the HealthKit store. There are eight different types of queries that you have available to use with HealthKit. This tutorial will cover the HKSampleQuery. We will look at each other type of query in separate posts.
HKSampleQuery
The Sample query is described as the general purpose query. The documentation explains that these are useful if you want to sort results or limit the total number of samples returned.
The initialiser shows us that we need the sample type, a predicate, a limit, and a sort descriptor. A results handler is provided.
init(sampleType: HKSampleType, predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void)
Lets step through each of these to see exactly what we can provide, and what we would get in return. So that we can work with a lot of data we will use step count. This allows all devices from the iPhone 5s upwards to contain a lot of data that can be queried.
Line 1 shows that we need a sampleType which is a HKSampleType. This is simply used to set the query to fetch the data you want. Later on, we will use this to specify we want step count data.
On line 2 the predicate can be declared, although note that it is optional. You may want to receive all data for step count, or you might want to limit what you get between two dates. It is here that you can decide to just pass in nil, or you can form a predicate prior to init and provide some dates such as “a date 7 days ago and today”.
Line 3 has the limit. This is just an integer set to how many results you want returning. Perhaps you are only interested in seeing 100 results and not all of them between two dates.
Line 4 is the sort descriptor. This is also optional, meaning that nil can be passed in. Note that it is also surrounded in square brackets which indicates that we can use more than 1 type of sort although typically you will use nil and handle your own sorting, or use a date type sort. For workouts you can sort by distance, time, and other pre-defined ways.
Line 5 is the results handler. We see that we receive the query, an array of HKSample objects (with the array being marked as optional… meaning we might not get any results), and then an optional error is passed.
A Simple HKSampleQuery
To get started we can look at a very simple query. Download the project from yesterday which contains the setup of HealthKit and gives us a good place to start from.
You can comment out the testCharachteristic() line if you wish, or just leave that in there and add a line below it.
} else { self.testCharachteristic() self.testSampleQuery() }
I added in a call to self.testSampleQuery()
Later on in ViewController.swift I added the following function and implemented a very simple HKSampleQuery:
func testSampleQuery() { 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) }
Lets take a look at what is happening here.
Line 2 we create the sample type. This is fairly boilerplate. If you wanted to see body mass, then just edit the .stepCount property on the end to .bodyMass although if you do make a change here, make sure you edit the readDataTypes constant Set to include bodyMass (see line 21 in the downloaded project).
Line 3 is where we initialise the query. Here we create query as a constant (because we need to execute the query later on). On this particular line we pass in the sampleType that we created on line 2.
Line 4 requires we pass in a predicate, or set it to nil. For this example I set it to nil to demonstrate what happens. We will look at predicates later on in this tutorial.
Line 5 we set the limit to HKObjectQueryNoLimit. Alternatively, you can specify a value here such as 10, 100, 23, or whatever you want. If you have no sort descriptor set on line 6 then a value on line 5 will return the oldest samples. So if your first step count was from 2013 then the very first X amount of items will be returned.
Line 6 is where we set the sort descriptor. For this part of the tutorial I set it at nil.
Line 6 also includes the query, results, and error in the results handler.
Line 7 is used to print the results. You can ignore the warning for now.
Line 10 is used to execute the query we created on line 3. When this finishes executing (which is done on a background thread) the results handler is called on line 6 which then runs the print statement on line 7.
If you go ahead and run the app now you will see a white screen for a while, and then eventually the query will finish executing and every step count recorded in Apple Health will then be printed to the console. The amount of time it will take to execute will depend on how many samples you have in Apple Health, and how new or old your device is (newer phone == quicker CPU). So sit patiently and wait for logs to appear.
You might notice that receiving every single step count recorded (which are typically stored in 1, 2, or 3 digit numbers) might be a little tedious to work with. So for step counts, the HKSampleQuery might not be best unless you put the query in a loop and set a predicate for yesterday, run it, then loop around and set it to the day before yesterday, and then run it… but again, you will receive many step counts for the day. To make step counts useful you would need to manually gather all the samples in the results array and then do your own calculations. Thankfully, Apple has provided other types of queries that will do this for you. I just wanted to highlight using this type of query so that you can see why you might not want to use it for stepCounts.
HKSampleQuery With Predicate
Where a HKSampleQuery can work well is for items that do not need to be added up to make a total for an hour or a total for the day. Body Mass is one that works well for this type of query. Perhaps you want to see your body mass records for the last 3 months. You can set a start date of 3 months ago, and an end date as today. I think most people would probably just record 1 weight per day for themselves. The bodyMass records do not require you add them up incrementally to get your total bodyMass for the day. They simple are what they are… so lets look at how we might use some dates in the predicate and then retrieve the samples we need.
On line 21 in ViewController.swift modify the readDataTypes Set to include bodyMass as follows:
let readDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!, HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.biologicalSex)!, HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.dateOfBirth)!, HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bodyMass)!]
When you next run the app it will recognise that bodyMass is a new item that needs permission from the user to access it. It will put the permissions sheet on screen to request access for this user.
Around line 33 in ViewController I added a new call to a function:
} else { self.testCharachteristic() //self.testSampleQuery() self.testSampleQueryWithPredicate() }
I added in testSampleQueryWithPredicate() to keep the code separate from the other sample query. I also commented out the testSampleQuery() call due to it taking a while to run. However, you can leave both queries running. Each of them run on their own background thread. Unless you have very little step count date, you will most certainly see bodyMass complete first, followed by stepCount.
Here is some sample code that shows how to use a predicate:
// 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) }
Lets step through this again.
Line 3 we create a sample type for bodyMass. Remember that a few moments ago we edited the readDataTypes to seek authorisation for this change.
Line 5 we set a date for “now” and call it today.
Line 6 we create a start date. To do this we use the current calendar, and call this instance method: date(byAdding:value:to:wrappingComponents:).
This takes a calendar component. I used .month in this case. A full list of available components can be found here.
We then provide a value. In this case we want to go back 3 months, so I entered -3. If you set the component to year then you might go back 1 year. Alternatively, you can go forwards by dates if you work from an older date (rather than todays date).
Finally we pass in the reference date which we called “today”.
Line 8 is used to create the predicate. We pass in the start date, the end date (called today), and set the options. There are only 2 options available which are strictEndDate and strictStartDate.
Line 10 we form the query, as we did for the query from earlier on in the tutorial. Instead of passing in a nil predicate, we pass in the predicate. We then execute the query on line 17.
Go ahead and run the app. If you have bodyMass stored in Apple Health, and granted permissions for the app to read this, you will see a list of all bodyMass entries recorded for the past 3 months.
HKSampleQuery With Sort Descriptors
One final way to test the HKSampleQuery is to specify a sort descriptor. There are various sort descriptors that HealthKit has defined. Two of the basic ones are HKSampleSortIdentifierStartDate and the similar named HKSampleSortIdentifierEndDate. These can be used for HKSample retrievals as documented here. If you are requesting workout data, you can find the sort descriptors here.
For bodyMass queries the start date or end date would be identical because the sample was taken at a moment of time and not over a duration of time. For that reason, we can use either to sort. To see the results of bodyMass sorted in reverse order, use the following:
let query = HKSampleQuery.init(sampleType: sampleType!, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)]) { (query, results, error) in print(results) }
The only change here is line 4 where we have replaced nil with the following:
[NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)]
We use the square brackets because sortDescriptors is expecting an array to be passed.
When to use a HKSampleQuery
The HKSampleQuery works well for types of requests where the data stored is the final value, such as your body weight (bodyMass) for the day. Where it is less appropriate for use is when samples provided are used together to provide a value for a day or for an hour. Consider step counts. Your Apple Watch and iPhone both count steps. These are recorded to HealthKit every few minutes. A daily step count is a combination of all these incremental updates throughout the day combined together with duplicates filtered out (ie, if the watch and phone both count steps at the same time, one of the numbers is removed). Although you could do your own calculations and query dates and times and combine them together for an hourly or daily count, there are better types of queries that can be used that can handle the complexities of this for you. We will look at another type of query in the next tutorial.
You can download the latest version of the project here.
Full code:
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() } } } } // 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) } 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.