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.
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
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: Workouts
30 mins
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:
-
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 isother
(all Prancercise workouts use theother
activity type). -
HKSource
denotes the app that provided the workout data to HealthKit. Whenever you callHKSource.default()
, you’re saying “this app.”sourcePredicate
gets all workouts where the source is, you guessed it, this app. - 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 withother
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.
- There is an optional array called
workouts
for storing workouts. Those are what you’ll load usingloadPrancerciseWorkouts(completion:)
from the previous section. - There is a method named
reloadWorkouts()
. You call it fromviewWillAppear(_:)
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.
override
keyword in front of them. The reason you need to use override
here is because WorkoutsTableViewController
is a subclass of UITableViewController
.
UITableViewController
already implements all the functions associated with UITableViewDatasource
. To get custom behavior, you need to override those default implementations.
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:
- You dequeue a cell from the table view.
- You get the row’s corresponding workout.
- You populate the main label with the start date of the workout.
- 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.
It’s a short workout, but boy can it burn. This new workout routine gives CrossFit a run for its money.