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.

4.5 (15) · 1 Review

Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

  1. To specify the type of sample you want to query (weight, height, etc.)
  2. Some additional parameters to help filter and sort the data. You can pass in an optional NSPredicate or an array of NSSortDescriptors 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.

  1. 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.
  2. 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()
}
  1. 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.
  2. 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.