Update Note: This tutorial has been updated for Swift 4, Xcode 9 and iOS 11 by Ted Bendixson. The original tutorial was written by Ernesto García.
HealthKit is an API that was introduced in iOS 8. It acts as a central repository for all health-related data, letting users build a biological profile and store workouts.
In this HealthKit tutorial, you will create a simple workout tracking app and learn:
- How to request permission and access HealthKit data
- How to read HealthKit data and display it in a UITableView
- How to write data to HealthKit’s central repository
Ready to start this HealthKit Exercise? Read on!
Note: To work through this HealthKit tutorial, you’ll need an active iOS developer account. Without one, you won’t be able to enable the HealthKit Capability and access the HealthKit Store.
Getting Started
The sample app tracks calories burned doing the latest celebrity endorsed workout routine. It should be obvious to Hollywood insiders and well-mannered socialites that I’m talking about Prancercise.
Download the starter project and open it in Xcode.
Build and run the app. You will see a skeleton of the user interface. Throughout the course of these next two articles, you will slowly add in the functionality.
Assigning a Team
HealthKit is a special framework. Your app can’t use it unless you have an active developer account. Once you have a developer account, you can assign your team.
Select PrancerciseTracker in the Project Navigator, and then select the PrancerciseTracker target. Select the General tab and click on the Team combo box.
Select the team associated with your developer account:
That was a piece of cake, right? Oops. I meant to say that was a low-calorie potato and red lentil soup stewed in a vegetable broth :].
Entitlements
HealthKit also has its own set of entitlements, and you will need to enable them in order to build apps that use the framework.
Open the Capabilities tab in the target editor, and turn on the HealthKit switch, as shown in the screenshot below:
Wait for Xcode to configure HealthKit for you. There usually isn’t a problem here, but you might run into some snags if you forgot to setup your Team and Bundle Identifier like you did in the previous section.
Done and done. Now you just need to ask the user for permission to use HealthKit.
Permissions
HealthKit deals with sensitive and private data. Not everyone feels so comfortable letting their apps access this information.
That’s why HealthKit has a robust privacy system. HealthKit only has access to the kinds of data your users agree to share with it. To build up a health profile for your Prancercise Tracker’s users, you need to be nice and ask for permission to access each type of data first.
Updating the Share Usage Descriptions
First, you need to describe why you are asking for health metrics from your users. Xcode gives you a way to specify this in your application’s Info.plist file.
Open Info.plist. Then add the following keys:
Privacy – Health Share Usage Description
Privacy – Health Update Usage Description
Both keys store text that display when the HeathKit authorization screen appears. The Health Share Usage Description goes under the section for data to be read from HealthKit. The Health Update Usage Description corresponds to data that gets written to HealthKit.
You can put anything you want in there. Typically it’s some explanation saying, “We will use your health information to better track Prancercise workouts.”
Do be aware that if those keys aren’t set, your app will crash when attempting to authorize HealthKit.
Authorizing HealthKit
Open HealthKitSetupAssistant.swift and take a peek inside. You will find an empty class with an error type and the body of a method you will use to authorize HealthKit.
class func authorizeHealthKit(completion: @escaping (Bool, Error?) -> Swift.Void) {
}
the authorizeHealthKit(completion:)
method accepts no parameters, and it has a completion handler that returns a boolean (success or failure) and an optional error in case something goes wrong. That’s what the two possible errors are for. You will pass them into the completion handler under two special circumstances.
Let’s break this process down. To authorize HealthKit, the authorizeHealthKit(completion:)
method will need to do these four things:
- Check to see if Healthkit is available on this device. If it isn’t, complete with failure and an error.
- Prepare the types of health data Prancercise Tracker will read and write to HealthKit.
- Organize those data into a list of types to be read and types to be written.
- Request Authorization. If it’s successful, complete with success.
Checking HealthKit Availability
First things first. You are going to check if HealthKit is available on the device.
Paste the following bit of code at the top of the authorizeHealthKit(completion:)
method:
//1. Check to see if HealthKit Is Available on this device
guard HKHealthStore.isHealthDataAvailable() else {
completion(false, HealthkitSetupError.notAvailableOnDevice)
return
}
You are going to interact with HKHealthStore
quite a lot. It represents the central repository that stores a user’s health-related data. HKHealthStore
’s isHealthDataAvailable()
method helps you figure out if the user’s current device supports HeathKit data.
The guard
statement stops the app from executing the rest of the authorizeHealthKit(completion:)
method’s logic if HealthKit isn’t available on the device. When this happens, the method completes with the notAvailableOnDevice
error. Your view controller can do something with that, or you can just log it to the console.
Preparing Data Types
Once you know HealthKit is available on your user’s device, it is time to prepare the types of data that will get read from and written to HealthKit.
HealthKit works with a type called HKObjectType
. Every type that goes into and out HealthKit’s central repository is some kind of HKObjectType
. You will also see HKSampleType
and HKWorkoutType
. Both inherit from HKObjectType
, so they’re basically the same thing.
Paste this next piece of code right after the first piece of code:
//2. Prepare the data types that will interact with HealthKit
guard let dateOfBirth = HKObjectType.characteristicType(forIdentifier: .dateOfBirth),
let bloodType = HKObjectType.characteristicType(forIdentifier: .bloodType),
let biologicalSex = HKObjectType.characteristicType(forIdentifier: .biologicalSex),
let bodyMassIndex = HKObjectType.quantityType(forIdentifier: .bodyMassIndex),
let height = HKObjectType.quantityType(forIdentifier: .height),
let bodyMass = HKObjectType.quantityType(forIdentifier: .bodyMass),
let activeEnergy = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned) else {
completion(false, HealthkitSetupError.dataTypeNotAvailable)
return
}
Wow, that’s big guard
statement! It’s also an excellent example of using a single guard
to unwrap multiple optionals.
In order to create an HKObjectType
for a given biological characteristic or quantity, you need to use either HKObjectType.characteristicType(forIdentifier:)
or HKObjectType.quantityType(forIdentifier:)
The characteristic types and the quantity types are both enums defined by the framework. HealthKit is loaded with these. There are so many different dimensions of health for your app to track that it makes my head spin whilst prancercising around the possibilities.
You will also notice that if a single characteristic or sample type is not available, the method will complete with an error. That’s intentional. Your app should always know exactly which HealthKit types it can work with, if any at all.
Preparing a list of data types to read and write
Now it’s time to prepare a list of types to read and types to write.
Paste this third bit of code into the authorizeHealthKit(completion:)
method, right after the second piece:
//3. Prepare a list of types you want HealthKit to read and write
let healthKitTypesToWrite: Set<HKSampleType> = [bodyMassIndex,
activeEnergy,
HKObjectType.workoutType()]
let healthKitTypesToRead: Set<HKObjectType> = [dateOfBirth,
bloodType,
biologicalSex,
bodyMassIndex,
height,
bodyMass,
HKObjectType.workoutType()]
HealthKit expects a set of HKSampleType
objects that represent the kinds of data your user can write, and it also expects a set of HKObjectType
objects for your app to read.
HKObjectType.workoutType()
is a special kind of HKObjectType
. It represents any kind of workout.
Authorizing HealthKit
The final part is the easiest. You just need to request authorization from HealthKit. Paste this last piece of code after the third piece:
//4. Request Authorization
HKHealthStore().requestAuthorization(toShare: healthKitTypesToWrite,
read: healthKitTypesToRead) { (success, error) in
completion(success, error)
}
These lines of code request authorization from HealthKit and then call your completion handler. They use the success and error variables passed in from HKHealthStore
’s requestAuthorization(toShare: read: completion:)
method.
You can think of it as a redirect. Instead of handling the completion inside of HealthKitSetupAssistant
, you are passing the buck to a view controller that can present an alert or take some other action.
The starter project already has an Authorize HealthKit button for this, and it invokes the method authorizeHealthKit()
in MasterViewController
. That sounds like the perfect place to call the new authorization method you just wrote.
Open MasterViewController.swift, locate authorizeHealthKit()
and paste this code into the body:
HealthKitSetupAssistant.authorizeHealthKit { (authorized, error) in
guard authorized else {
let baseMessage = "HealthKit Authorization Failed"
if let error = error {
print("\(baseMessage). Reason: \(error.localizedDescription)")
} else {
print(baseMessage)
}
return
}
print("HealthKit Successfully Authorized.")
}
This code uses the authorizeHealthKit(completion:)
method you just implemented. When it is finished, it prints a message to the console to let you know if HealthKit was successfully authorized.
Build and run. Tap Authorize HealthKit in the main view, and you will see an authorization screen pop up:
Turn on all the switches, scrolling the screen to see all of them, and click Allow. You’ll see a message like this in Xcode’s console:
HealthKit Successfully Authorized.
Great! Your app has access to HealthKit’s central repository. Now it’s time to start tracking things.
Characteristics and Samples
In this section, you will learn:
- How to read your user’s biological characteristics.
- How to read and write different types of samples (weight, height, etc.)
Biological characteristics tend to be the kinds of things that don’t change, like your blood type. Samples represent things that often do change, like your weight.
In order to properly track the effectiveness of a Prancercise workout regimen, the Prancercise Tracker app needs to get a sample of your user’s weight and height. Put together, these samples can be used to calculate Body Mass Index (BMI).
Note: Body Mass Index (BMI) is a widely used indicator of body fat, and it’s calculated from the weight and height of a person. Learn more about it here.
Reading Characteristics
The Prancercise Tracker app doesn’t write biological characteristics. It reads them from HealthKit. That means those characteristics need to be stored in HeathKit’s central repository first.
If you haven’t already done this, it’s time to tell HeathKit some more about yourself.
Open the Health App on your device or in the simulator. Select the Health Data tab. Then tap on the profile icon in the top right hand corner to view your health profile. Hit Edit, and enter information for Date of Birth, Sex, Blood Type:
Now that HealthKit knows your Date of Birth, Sex, and Blood Type, it’s time to read those characteristics into Prancercise Tracker.
Go back to Xcode and open ProfileDataStore.swift. The ProfileDataStore
class represents your point of access to all of the health-related data for your users.
Paste the following method into ProfileDataStore
:
class func getAgeSexAndBloodType() throws -> (age: Int,
biologicalSex: HKBiologicalSex,
bloodType: HKBloodType) {
let healthKitStore = HKHealthStore()
do {
//1. This method throws an error if these data are not available.
let birthdayComponents = try healthKitStore.dateOfBirthComponents()
let biologicalSex = try healthKitStore.biologicalSex()
let bloodType = try healthKitStore.bloodType()
//2. Use Calendar to calculate age.
let today = Date()
let calendar = Calendar.current
let todayDateComponents = calendar.dateComponents([.year],
from: today)
let thisYear = todayDateComponents.year!
let age = thisYear - birthdayComponents.year!
//3. Unwrap the wrappers to get the underlying enum values.
let unwrappedBiologicalSex = biologicalSex.biologicalSex
let unwrappedBloodType = bloodType.bloodType
return (age, unwrappedBiologicalSex, unwrappedBloodType)
}
}
The getAgeSexAndBloodType()
method accesses HKHealthStore
, asks for the user’s date of birth, biological sex, and blood type. It also calculates the user’s age using the date of birth.
- You may have noticed this method can throw an error. It happens whenever the date of birth, biological sex, or blood type haven’t been saved in HealthKit’s central repository. Since you just entered this information into your Health app, no error should be thrown.
- Using the
Calendar
class, you can transform any given date into a set of Date Components. These are really handy when you want to get the year for a date. This code simply gets your birth year, the current year, and then calculates the difference. - The “unwrapped” variables are named that way to make it clear that you have to access the underlying enum from a wrapper class (
HKBiologicalSexObject
andHKBloodTypeObject
).
Updating The User Interface
If you were to build and run now, you wouldn’t see any change to the UI because you haven’t connected this logic to it yet.
Open ProfileViewController.swift and find the loadAndDisplayAgeSexAndBloodType()
method.
This method will use your ProfileDataStore
to load the biological characteristics into the user interface.
Paste the following lines of code into the loadAndDisplayAgeSexAndBloodType()
method:
do {
let userAgeSexAndBloodType = try ProfileDataStore.getAgeSexAndBloodType()
userHealthProfile.age = userAgeSexAndBloodType.age
userHealthProfile.biologicalSex = userAgeSexAndBloodType.biologicalSex
userHealthProfile.bloodType = userAgeSexAndBloodType.bloodType
updateLabels()
} catch let error {
self.displayAlert(for: error)
}
This block of code loads age, sex, and blood type as a tuple. It then sets those fields on a local instance of the UserHealthProfile
model. Finally, it updates the user interface with the new fields on UserHealthProfile
by calling the updateLabels()
method.
Because ProfileDataStore
’s getAgeSexAndBloodType()
method can throw an error, your ProfileViewController
has to handle it. In this case, you simply take the error and present it inside of an alert with an “O.K.” button.
All of this is great, but there’s one catch. The updateLabels()
method doesn’t do anything yet. It’s just an empty declaration. Let’s hook it up to the user interface for real this time.
Locate the updateLabels()
method and paste these lines of code into its body:
if let age = userHealthProfile.age {
ageLabel.text = "\(age)"
}
if let biologicalSex = userHealthProfile.biologicalSex {
biologicalSexLabel.text = biologicalSex.stringRepresentation
}
if let bloodType = userHealthProfile.bloodType {
bloodTypeLabel.text = bloodType.stringRepresentation
}
This code is pretty straightforward. If you user has set an age, it will get formatted an put into the label. The same goes for biological sex and bloodType. The stringRepresentation
variable converts the enum to a string for display purposes.
Build and run the app. Go into the Profile & BMI screen. Tap on the Read HealthKit Data button.
If you entered your information into the Health app earlier, it should appear in the labels on this screen. If you didn’t, you will get an error message.
Cool! You’re reading and displaying data directly from HealthKit.
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.
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.
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.
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.
Saving Samples
Prancercise Tracker already has a convenient body mass index calculator. Let’s use it to record a sample of your user’s BMI.
Open ProfileDataStore.swift and add the following method:
class func saveBodyMassIndexSample(bodyMassIndex: Double, date: Date) {
//1. Make sure the body mass type exists
guard let bodyMassIndexType = HKQuantityType.quantityType(forIdentifier: .bodyMassIndex) else {
fatalError("Body Mass Index Type is no longer available in HealthKit")
}
//2. Use the Count HKUnit to create a body mass quantity
let bodyMassQuantity = HKQuantity(unit: HKUnit.count(),
doubleValue: bodyMassIndex)
let bodyMassIndexSample = HKQuantitySample(type: bodyMassIndexType,
quantity: bodyMassQuantity,
start: date,
end: date)
//3. Save the same to HealthKit
HKHealthStore().save(bodyMassIndexSample) { (success, error) in
if let error = error {
print("Error Saving BMI Sample: \(error.localizedDescription)")
} else {
print("Successfully saved BMI Sample")
}
}
}
Some of this will seem familiar. As with other sample types, you first need to make sure the sample type is available in HealthKit.
- In this case, the code checks to see if there is a quantity type for body mass index. If there is, it gets used to create a quantity and quantity sample. If not, the app intentionally crashes.
-
The
count()
method onHKUnit
is for a special case when there isn’t a clear unit for the type of sample you are storing. At some point in the future, there may be a unit assigned to body mass index, but for now this more generic unit works just fine. -
HKHealthStore
saves the sample and lets you know if the process was successful from a trailing closure. You could do more with this, but for the now the app just prints to the console.
Almost done. Let’s hook this thing up the user interface.
Open ProfileViewController.swift, find the saveBodyMassIndexToHealthKit()
method. This method gets called when the user taps the Save BMI button in the table view.
Paste the following lines of code into the method:
guard let bodyMassIndex = userHealthProfile.bodyMassIndex else {
displayAlert(for: ProfileDataError.missingBodyMassIndex)
return
}
ProfileDataStore.saveBodyMassIndexSample(bodyMassIndex: bodyMassIndex,
date: Date())
You will recall that the body mass index is a computed property which returns a value when both height and weight samples have been loaded from HealthKit. This code attempts to compute that property, and if it can, it gets passed to the savedBodyMassIndexSample(bodyMassIndex: date:)
method you just wrote.
It also shows a handy alert message if body mass index can’t be computed for some reason.
Build and run Prancercise Tracker one final time. Go into the Profile & BMI screen. Load your data from HeathKit, then tap the Save BMI button.
Take a look at the console. Do you see this?
Successfully saved BMI Sample
If you do, congratulations! Your BMI sample is now stored in HealthKit’s central repository. Let’s see if we can find it.
Open the Health app, tap the Health Data tab, Tap on Body Measurements in the table view, and then tap on Body Mass Index.
Unless you regularly record your body mass index (as all high profile prancercisers do), you should see a single data point like this one:
Sweet. You can see the sample from an app of your own creation, right there in Apple’s Health app. Imagine the possibilities.
Where To Go From Here?
Here is the example app with all of the modifications we have made up to this point.
Congratulations, you’ve got some hands-on experience with HealthKit! You now know how to request permissions, read biological characteristics, and read and write samples.
If you want to learn more, stay tuned for the next part of this HealthKit tutorial series where you’ll learn more about a more complex type of data: workouts.
In the meantime, if you have any questions or comments, please join the forum discussion below!
The post HealthKit Tutorial With Swift: Getting Started appeared first on Ray Wenderlich.