HealthKit Tutorial With Swift: Workouts

This HealthKit tutorial shows you step by step how to track workouts using the HealthKit APIs by integrating an app with the system’s Health app. By Felipe Laso-Marsetti.

4.1 (7) · 2 Reviews

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

Querying Workouts

Now, you can save a workout, but you also need a way to load workouts from HealthKit. You’ll add a new method to WorkoutDataStore to do that.

Paste the following method after save(prancerciseWorkout:completion:) in WorkoutDataStore.swift:

class func loadPrancerciseWorkouts(completion:
  @escaping ([HKWorkout]?, Error?) -> Void) {
  //1. Get all workouts with the "Other" activity type.
  let workoutPredicate = HKQuery.predicateForWorkouts(with: .other)
  
  //2. Get all workouts that only came from this app.
  let sourcePredicate = HKQuery.predicateForObjects(from: .default())
  
  //3. Combine the predicates into a single predicate.
  let compound = NSCompoundPredicate(andPredicateWithSubpredicates:
    [workoutPredicate, sourcePredicate])
  
  let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate,
                                        ascending: true)
}

If you followed the previous HealthKit tutorial, much of this code will look familiar. The predicates determine what types of HeathKit data you’re looking for, and the sort descriptor tells HeathKit how to sort the samples it returns. Here’s what’s going on in the code above:

  1. HKQuery.predicateForWorkouts(with:) is a special method that gives you a predicate for workouts with a certain activity type. In this case, you’re loading any type of workout in which the activity type is other (all Prancercise workouts use the other activity type).
  2. HKSource denotes the app that provided the workout data to HealthKit. Whenever you call HKSource.default(), you’re saying “this app.” sourcePredicate gets all workouts where the source is, you guessed it, this app.
  3. Those of you with Core Data experience may also be familiar with NSCompoundPredicate. It provides a way to bring one or more filters together. The final result is a query that gets you all workouts with other as the activity type and Prancercise Tracker as the source app.

Now that you have your predicate, it’s time to initiate the query. Add the following code to the end of the method:

let query = HKSampleQuery(
  sampleType: .workoutType(),
  predicate: compound,
  limit: 0,
  sortDescriptors: [sortDescriptor]) { (query, samples, error) in
    DispatchQueue.main.async {
      guard 
        let samples = samples as? [HKWorkout],
        error == nil 
        else {
          completion(nil, error)
          return
      }
      
      completion(samples, nil)
    }
  }

HKHealthStore().execute(query)

In the completion handler, you unwrap the samples as an array of HKWorkout objects. That’s because HKSampleQuery returns an array of HKSample by default, and you need to cast them to HKWorkout to get all the useful properties like start time, end time, duration and energy burned.

Loading Workouts Into the User Interface

You wrote a method that loads workouts from HealthKit. Now it’s time to take those workouts and use them to populate a table view. Some of the setup is already done for you.

Open WorkoutsTableViewController.swift and take a look around. You’ll see a few things.

  1. There is an optional array called workouts for storing workouts. Those are what you’ll load using loadPrancerciseWorkouts(completion:) from the previous section.
  2. There is a method named reloadWorkouts(). You call it from viewWillAppear(_:) whenever the view for this screen appears. Every time you navigate to this screen, the workouts refresh.

To populate the user interface with data, you’ll load the workouts and hook up the table view’s dataSource.

Paste the following lines of code into reloadWorkouts():

WorkoutDataStore.loadPrancerciseWorkouts { (workouts, error) in
  self.workouts = workouts
  self.tableView.reloadData()
}

Here, you load the workouts from the WorkoutDataStore. Then, inside the completion handler, you assign the workouts to the local workouts property and reload the table view with the new data.

At this point, you may have noticed there is still no way to get the data from the workouts to the table view. To solve that, you’ll put in place the table view’s dataSource.

Paste these lines of code at the bottom of the file, right after the closing curly brace:

// MARK: - UITableViewDataSource
extension WorkoutsTableViewController {
  override func tableView(_ tableView: UITableView,
                          numberOfRowsInSection section: Int) -> Int {
    return workouts?.count ?? 0
  }
}

This says you want the number of rows to correspond to the number of workouts you have loaded from HealthKit. Also, if you haven’t loaded any workouts from HealthKit, there are no rows and the table view will appear empty.

UITableViewController already implements all the functions associated with UITableViewDatasource. To get custom behavior, you need to override those default implementations.

Note: You might be used to seeing these methods without the override keyword in front of them. The reason you need to use override here is because WorkoutsTableViewController is a subclass of UITableViewController.

Now, you’ll tell the cells what to display. Paste this method right before the closing curly brace of the extension:

override func tableView(_ tableView: UITableView,
                cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  guard let workouts = workouts else {
    fatalError("""
      CellForRowAtIndexPath should \
      not get called if there are no workouts
      """)
  }
    
  //1. Get a cell to display the workout in
  let cell = tableView.dequeueReusableCell(withIdentifier:
    prancerciseWorkoutCellID, for: indexPath)
    
  //2. Get the workout corresponding to this row
  let workout = workouts[indexPath.row]
    
  //3. Show the workout's start date in the label
  cell.textLabel?.text = dateFormatter.string(from: workout.startDate)
    
  //4. Show the Calorie burn in the lower label
  if let caloriesBurned =
      workout.totalEnergyBurned?.doubleValue(for: .kilocalorie()) {
    let formattedCalories = String(format: "CaloriesBurned: %.2f",
                                   caloriesBurned)
      
    cell.detailTextLabel?.text = formattedCalories
  } else {
    cell.detailTextLabel?.text = nil
  }
    
  return cell
}

All right! This is where the magic happens:

  1. You dequeue a cell from the table view.
  2. You get the row’s corresponding workout.
  3. You populate the main label with the start date of the workout.
  4. If a workout has its totalEnergyBurned property set to something, then you convert it to a double using kilocalories as the conversion. Then, you format the string and display it in the cell’s detail label.

Most of this is very similar to the previous HealthKit tutorial. The only new thing is the unit conversion for calories burned.

Build and run the app. Go to Prancercise Workouts, tap the + button, track a short Prancercise workout, tap Done and take a look at the table view.

HealthKit tutorial

Note: If you’re getting an error while trying to save your workout, make sure you’ve tapped Authorize HealthKit on the app’s main screen and completed the authorization process.

It’s a short workout, but boy can it burn. This new workout routine gives CrossFit a run for its money.