HealthKit Tutorial With Swift: Getting Started
Learn how to request permission to access HealthKit data, as well as read and write data to HealthKit’s central repository in this HealthKit tutorial. By Ted Bendixson.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
HealthKit Tutorial With Swift: Getting Started
30 mins
- Getting Started
- Assigning a Team
- Entitlements
- Permissions
- Updating the Share Usage Descriptions
- Authorizing HealthKit
- Checking HealthKit Availability
- Preparing Data Types
- Preparing a list of data types to read and write
- Authorizing HealthKit
- Characteristics and Samples
- Reading Characteristics
- Updating The User Interface
- Querying Samples
- Displaying Samples in the User Interface
- Saving Samples
- Where To Go From Here?
Querying Samples
Now it’s time to read your user’s weight and height. This will be used to calculate and display their BMI in the profile view.
Biological characteristics are easy to access because they almost never change. Samples require a much more sophisticated approach. They use HKQuery
, more specifically HKSampleQuery
.
To query samples from HealthKit, you will need:
- To specify the type of sample you want to query (weight, height, etc.)
- Some additional parameters to help filter and sort the data. You can pass in an optional
NSPredicate
or an array ofNSSortDescriptors
to do this.
Note: If you’re familiar with Core Data, you probably noticed some similarities. An HKSampleQuery
is very similar to an NSFetchedRequest
for an entity type, where you specify the predicate and sort descriptors, and then ask the Object context to execute the query to get the results.
Note: If you’re familiar with Core Data, you probably noticed some similarities. An HKSampleQuery
is very similar to an NSFetchedRequest
for an entity type, where you specify the predicate and sort descriptors, and then ask the Object context to execute the query to get the results.
Once your query is setup, you simply call HKHealthStore
’s executeQuery()
method to fetch the results.
For Prancercise Tracker, you are going to create a single generic function that loads the most recent samples of any type. That way, you can use it for both weight and height.
Open ProfileDataStore.swift and paste the following method into the class, just below the getAgeSexAndBloodType()
method:
class func getMostRecentSample(for sampleType: HKSampleType,
completion: @escaping (HKQuantitySample?, Error?) -> Swift.Void) {
//1. Use HKQuery to load the most recent samples.
let mostRecentPredicate = HKQuery.predicateForSamples(withStart: Date.distantPast,
end: Date(),
options: .strictEndDate)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate,
ascending: false)
let limit = 1
let sampleQuery = HKSampleQuery(sampleType: sampleType,
predicate: mostRecentPredicate,
limit: limit,
sortDescriptors: [sortDescriptor]) { (query, samples, error) in
//2. Always dispatch to the main thread when complete.
DispatchQueue.main.async {
guard let samples = samples,
let mostRecentSample = samples.first as? HKQuantitySample else {
completion(nil, error)
return
}
completion(mostRecentSample, nil)
}
}
HKHealthStore().execute(sampleQuery)
}
This method takes in a sample type (height, weight, bmi, etc.). Then it builds a query to get the most recent sample for that type. If you pass in the sample type for height, you will get back your latest height entry.
There is a lot going on here. I will pause to break down a few things.
-
HKQuery
has a number of methods that can help you filter your HealthKit sample queries. It’s worth taking a look at them. In this case, we are using the built-in date window predicate. - Querying samples from HealthKit is an asynchronous process. That is why the code in the completion handler occurs inside of a Dispatch block. You want the completion handler to happen on the main thread, so the user interface can respond to it. If you don’t do this, the app will crash.
If all goes well, your query will execute and you will get a nice and tidy sample returned to the main thread where your ProfileViewController
can put its contents into a label. Let’s do that part now.
Displaying Samples in the User Interface
If you recall from the earlier section, you loaded the data from HealthKit, saved it to a model in ProfileViewController
, and then updated the content in the labels using ProfileViewController
’s updateLabels()
method.
All you need to do now is extend that process by adding a function that loads the samples, processes them for the user interface, and then calls updateLabels()
to populate the labels with text.
Open ProfileViewController.swift, locate the loadAndDisplayMostRecentHeight()
method, and paste the following code into the body:
//1. Use HealthKit to create the Height Sample Type
guard let heightSampleType = HKSampleType.quantityType(forIdentifier: .height) else {
print("Height Sample Type is no longer available in HealthKit")
return
}
ProfileDataStore.getMostRecentSample(for: heightSampleType) { (sample, error) in
guard let sample = sample else {
if let error = error {
self.displayAlert(for: error)
}
return
}
//2. Convert the height sample to meters, save to the profile model,
// and update the user interface.
let heightInMeters = sample.quantity.doubleValue(for: HKUnit.meter())
self.userHealthProfile.heightInMeters = heightInMeters
self.updateLabels()
}
- This method starts by creating a Height sample type. It then passes that sample type to the method you just wrote, which will return the most recent height sample recorded to HealthKit.
-
Once a sample is returned, the height is converted to meters and stored on the
UserHealthProfile
model. Then the labels get updated.
Note: You usually want to convert your quantity sample to some standard unit. To do that, the code above takes advantage of HKQuantitySample’s doubleValue(for:)
method which lets you pass in a HKUnit
matching what you want (in this case meters).
You can construct various types of HKUnits using some common class methods made available through HealthKit. To get meters, you just use the meter()
method on HKUnit
and you’re good to go.
Note: You usually want to convert your quantity sample to some standard unit. To do that, the code above takes advantage of HKQuantitySample’s doubleValue(for:)
method which lets you pass in a HKUnit
matching what you want (in this case meters).
You can construct various types of HKUnits using some common class methods made available through HealthKit. To get meters, you just use the meter()
method on HKUnit
and you’re good to go.
That covers height. What about weight? It’s very similar, but you will need to fill in the body for the loadAndDisplayMostRecentWeight()
method in ProfileViewController
.
Paste the following code into the loadAndDisplayMostRecentWeight()
method body:
guard let weightSampleType = HKSampleType.quantityType(forIdentifier: .bodyMass) else {
print("Body Mass Sample Type is no longer available in HealthKit")
return
}
ProfileDataStore.getMostRecentSample(for: weightSampleType) { (sample, error) in
guard let sample = sample else {
if let error = error {
self.displayAlert(for: error)
}
return
}
let weightInKilograms = sample.quantity.doubleValue(for: HKUnit.gramUnit(with: .kilo))
self.userHealthProfile.weightInKilograms = weightInKilograms
self.updateLabels()
}
It’s the exact same pattern. You create the type of sample you want to retrieve, ask HealthKit for it, do some unit conversions, save to your model, and update the user interface.
At this point, you might think you’re finished but there’s one more thing you need to do. The updateLabels()
function isn’t aware of the new data you’ve made available to it. Let’s change that.
Add the following lines to the updateLabels()
function, just below the part where you unwrap bloodType to display it in a label:
if let weight = userHealthProfile.weightInKilograms {
let weightFormatter = MassFormatter()
weightFormatter.isForPersonMassUse = true
weightLabel.text = weightFormatter.string(fromKilograms: weight)
}
if let height = userHealthProfile.heightInMeters {
let heightFormatter = LengthFormatter()
heightFormatter.isForPersonHeightUse = true
heightLabel.text = heightFormatter.string(fromMeters: height)
}
if let bodyMassIndex = userHealthProfile.bodyMassIndex {
bodyMassIndexLabel.text = String(format: "%.02f", bodyMassIndex)
}
Following the original pattern in the updateLabels()
function, it unwraps the height, weight, and body mass index on your UserHealthProfile
model. If those are available, it generates the appropriate strings and puts them in the labels. MassFormatter
and LengthFormatter
do the work of converting your quantities to strings.
Body Mass Index isn’t actually stored on the UserHealthProfile
model. It’s a computed property that does the calculation for you.
Command click on the bodyMassIndex
property, and you will see what I mean:
var bodyMassIndex: Double? {
guard let weightInKilograms = weightInKilograms,
let heightInMeters = heightInMeters,
heightInMeters > 0 else {
return nil
}
return (weightInKilograms/(heightInMeters*heightInMeters))
}
Body Mass Index is an optional property, meaning it can return nil if neither height nor weight are set (or if they are set to some number that doesn’t make any sense). The actual calculation is just the weight divided by height squared.
Note: You’ll be stuck soon if you’ve not added data in the HealthKit store for the app to read. If you haven’t already, you need to create some height and weight samples at the very least.
Open the Health App, and go to the Health Data Tab. There, select the Body Measurements option, then choose Weight and then Add Data Point to add a new weight sample. Repeat the process for the Height.
Note: You’ll be stuck soon if you’ve not added data in the HealthKit store for the app to read. If you haven’t already, you need to create some height and weight samples at the very least.
Open the Health App, and go to the Health Data Tab. There, select the Body Measurements option, then choose Weight and then Add Data Point to add a new weight sample. Repeat the process for the Height.
At this point, Prancercise Tracker should be able to read a recent sample of your user’s weight and height, then display it in the labels.
Build and run. Navigate to Profile & BMI. Then tap the Read HealthKit Data button.
Awesome! You just read your first samples from the HealthKit store and used them to calculate the BMI.