HealthKit Tutorial With Swift: Workouts [FREE]
source link: https://www.tuicool.com/articles/hit/za2qMfz
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Update note : Felipe Laso-Marsetti updated this tutorial for Swift 4.2, Xcode 10 and iOS 12. Ernesto García wrote the original.
Welcome back to our HealthKit tutorial series!
In the first part of the series , you learned the basics of working with HealthKit: reading and writing data.
In this second and final part of the series, you’ll learn how to work with a more complex type of data: workouts .
This project picks up where the previous HealthKit tutorial left off. If you don’t have the project already, then download the materials for this tutorial using the Download materials button at the top or bottom of the tutorial.
Get ready to take another rep in your HealthKit workout! :]
Getting Started
In your day-to-day life, a workout is a simple thing. It’s some period of time in which you increase physical exertion doing some sort of activity.
Most workouts have one more of the following attributes:
- Activity type (running, cycling, Prancercising, etc.)
- Distance
- Start and end time
- Duration
- Energy burned
HealthKit thinks of workouts in much the same way. A workout is a container for these types of information, taken as a collection of samples. A given workout might contain heart rate samples, distance samples and an activity type to categorize them.
Continuing from the previous HealthKit tutorial , you’re going to track a special kind of workout: Prancercise.
The starter project already contains a view controller that gives you a place to track your Prancercise workouts. To see it, navigate to Prancercise Workouts and then tap the + button.
This view contains a button that starts a Prancercise workout. Tap the button once. The app will start to track your Prancercise session, showing you the start time and duration.
Tap the button a second time. You’ll see that the current Prancercise session will stop. At this point, you can tap Done to record the workout, or you can tap the New Prancercise button to start a new workout session (be aware this erases the old session).
Saving Workouts
For the moment, the app doesn’t actually do anything when you tap Done to save your workout. You’re going to change that.
First, a little bit of context: Open Workout.swift and take a look around. You should see a struct named PrancerciseWorkout
.
struct PrancerciseWorkout { var start: Date var end: Date init(start: Date, end: Date) { self.start = start self.end = end } var duration: TimeInterval { return end.timeIntervalSince(start) } var totalEnergyBurned: Double { let prancerciseCaloriesPerHour: Double = 450 let hours: Double = duration / 3600 let totalCalories = prancerciseCaloriesPerHour * hours return totalCalories } }
PrancerciseWorkout
is the model the app uses to store information related to a workout. The app creates one every time you tap the Done button after finishing your Prancercise session.
Each PrancerciseWorkout
keeps track of its:
- Start and end time
- Duration
- Total calories burned
Your app feeds these values into HealthKit when you save your workout.
Note : Here, you’re assuming a somewhat intense Prancercise pace with aggressive ankle weights and loud fist-pumping musical accompaniment. Hence, the workout burns 450 calories per hour. Eat it, Zumba!
Now that you understand what a PrancerciseWorkout
contains, you’ll save one.
Open WorkoutDataStore.swift and take a look at save(prancerciseWorkout:completion:)
. This is what you’ll use to save your Prancercise workout to HealthKit.
Add the following lines of code into that method:
let healthStore = HKHealthStore() let workoutConfiguration = HKWorkoutConfiguration() workoutConfiguration.activityType = .other
The first thing you do is create a health store and workout configuration to store data into HealthKit. Next, add the following to the end of the method:
let builder = HKWorkoutBuilder(healthStore: healthStore, configuration: workoutConfiguration, device: .local()) builder.beginCollection(withStart: prancerciseWorkout.start) { (success, error) in guard success else { completion(false, error) return } }
Use the HKWorkoutBuilder
class (new to iOS 12) to incrementally construct a workout and begin collecting workout data. Next, add the following just before the closing brace of `beginCollection(withStart:completion:)`’s completion handler to get the number of calories burned:
guard let quantityType = HKQuantityType.quantityType( forIdentifier: .activeEnergyBurned) else { completion(false, nil) return } let unit = HKUnit.kilocalorie() let totalEnergyBurned = prancerciseWorkout.totalEnergyBurned let quantity = HKQuantity(unit: unit, doubleValue: totalEnergyBurned)
You may remember HKQuantity
from the earlier HealthKit tutorial. You used it to read and write your user’s height, weight, and body mass index. Here, you’re creating a quantity to store the total calories burned in this sample. After that, you can create a new sample. Add this code immediately following the code you just added:
let sample = HKCumulativeQuantitySeriesSample(type: quantityType, quantity: quantity, start: prancerciseWorkout.start, end: prancerciseWorkout.end)
HKCumulativeQuantitySeriesSample
is a new sample type that stores the total data for a workout rather than individual values. You can use this, for example, to collect the total distance you ran in a basketball game by adding up all the individual samples. Finally, add the sample to the builder by adding the following code to the method:
//1. Add the sample to the workout builder builder.add([sample]) { (success, error) in guard success else { completion(false, error) return } //2. Finish collection workout data and set the workout end date builder.endCollection(withEnd: prancerciseWorkout.end) { (success, error) in guard success else { completion(false, error) return } //3. Create the workout with the samples added builder.finishWorkout { (_, error) in let success = error == nil completion(success, error) } } }
Here’s what’s going on in the code above:
- With the sample ready, it’s time to add it to the workout that you’re building.
- Here, you finish the collection of workout data and set the end date for the workout.
- You create the workout with all the samples that have been added. Note how you’ve been nesting calls within completion blocks. This is to ensure that no race conditions occur and that each step is finished before attempting the next. If you’re not seeing the right data in your apps compared to HealthKit, it may be due to the workout builder’s more block-based, asynchronous nature.
Quite fittingly, a Prancercise Workout is an activity that can only be categorized as Other , but you can pick from any number of supported activity types if you like. :]
You may have also noticed that you can tell HealthKit (when creating the HKWorkoutBuilder
) what device the workout was recorded on. This can be useful when querying data later.
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 withCore 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.
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.
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.
Adding Samples to Workouts
Up to this point, you have assumed that a Prancercise workout is composed of a single workout session. But if you find Prancercise a little too intense, you might want to break it into a few short intervals.
With samples , you can record many exercise intervals under the same workout. It’s a way to give HealthKit a more detailed view of what you did during your workout routine.
You can add all kinds of samples to a workout. If you want, you can add distance, calories burned, heart rate and more.
Because Prancercise is more of a dance routine, this HealthKit tutorial will focus on calorie burn samples.
Making Model Updates
This is a new way of thinking about Prancercise workouts, and that means it’s time to change the model.
Instead of using a single Prancercise workout model, you need the concept of a workout interval representing a short session. That way, a single PrancerciseWorkout
becomes a wrapper or container for the workout intervals that store the starts and stops you took during your routine.
Open Workout.swift . Rename PrancerciseWorkout
to PrancerciseWorkoutInterval
so it looks like this:
struct PrancerciseWorkoutInterval { var start: Date var end: Date var duration: TimeInterval { return end.timeIntervalSince(start) } var totalEnergyBurned: Double { let prancerciseCaloriesPerHour: Double = 450 let hours: Double = duration / 3600 let totalCalories = prancerciseCaloriesPerHour * hours return totalCalories } }
You can see that nothing else has changed. What was once an entire workout is now a piece of a workout.
Paste the following code beneath the struct declaration for PrancerciseWorkoutInterval
:
struct PrancerciseWorkout { var start: Date var end: Date var intervals: [PrancerciseWorkoutInterval] init(with intervals: [PrancerciseWorkoutInterval]) { self.start = intervals.first!.start self.end = intervals.last!.end self.intervals = intervals } }
Now, a full PrancerciseWorkout
is composed of an array of PrancerciseWorkoutInterval
values. The workout starts when the first item in the array starts, and it ends when the last item in the array ends.
This is a nice way of representing a workout as something composed of time intervals, but it’s still missing a concept of duration and total energy burned. The code won’t compile until you have defined those.
Functional programming comes to the rescue here. You can use reduce
to sum up the duration and total energy burned properties on PrancerciseWorkoutInterval
.
Paste the following computed properties below init(with:)
in PrancerciseWorkout
:
var totalEnergyBurned: Double { return intervals.reduce(0) { (result, interval) in result + interval.totalEnergyBurned } } var duration: TimeInterval { return intervals.reduce(0) { (result, interval) in result + interval.duration } }
reduce
takes a starting value — in this case zero — and a closure that takes in the result of the previous computation. It does this for each item in the array.
To calculate the total energy burned, reduce starts at zero and adds zero to the first energy burn value in the array. Then it takes the result and adds it to the next value in the array, and so on. Once it has hit the end of the array, you get a sum total of all energy burned throughout your workout.
Reduce is a handy function for summing up arrays. If you would like to read more about functional programming and its awesomeness, check out this article .
Workout Sessions
You are almost finished upgrading the models. Open WorkoutSession.swift and take a look.
WorkoutSession
is used to store data related to the current PrancerciseWorkout
being tracked. Since you added in this concept of workout intervals to PrancerciseWorkout
, WorkoutSession
needs to add new intervals each time you start and end a Prancercise session.
Inside of the WorkoutSession
class, locate the line that declares the state variable. It looks like this:
var state: WorkoutSessionState = .notStarted
Below it, add a new line that declares an array of PrancerciseWorkoutInterval
values:
var intervals: [PrancerciseWorkoutInterval] = []
When you finish a Prancercise session, a new interval will get added to this array. You’ll add a function to do that.
Add the following method below clear()
in WorkoutSession
:
private func addNewInterval() { let interval = PrancerciseWorkoutInterval(start: startDate, end: endDate) intervals.append(interval) }
This method takes the start and end dates from the workout session, and it creates a PrancerciseWorkoutInterval
from them. Note that the start and end dates get reset every time a Prancercise session begins and ends.
Now that you have a way to add workout intervals, replace the end()
method in WorkoutSession
with this:
func end() { endDate = Date() addNewInterval() state = .finished }
You can see that, right after you set the end date for the session, you call addNewInterval()
.
It’s also important to clear out the array whenever the workout session needs to get cleared. So, add this at the end of clear()
:
intervals.removeAll()
removeAll()
does exactly what it says. It removes ALL. :]
There’s one more modification. completeWorkout
needs to use the intervals to create a new PrancerciseWorkout
object.
Replace the declaration of completeWorkout
with this:
var completeWorkout: PrancerciseWorkout? { guard state == .finished, intervals.count > 0 else { return nil } return PrancerciseWorkout(with: intervals) }
There you have it. Since this property is optional, you only want it to return a full PrancerciseWorkout
when the WorkoutSession
is finished and you have recorded at least one interval.
Every time you stop the workout, a new PrancerciseWorkoutInterval
with the current start and stop dates gets added to the list. Once the user taps Done to save the workout, this code generates a full-blown PrancerciseWorkout
entity using the intervals recorded during the many sessions.
There’s no need to change the code in CreateWorkoutViewController
. The button actions call the same start()
, end()
, and clear()
methods. The only difference is that instead of working with a single time interval, WorkoutSession
generates and stores many.
Build and run. Play around with starting and stopping your Prancercise workout, then tap Done to add your workout to the list.
One thing to note is that, although the app records accurate Prancercise workouts to HealthKit, there aren’t any samples attached. You need a way to convert PrancerciseWorkoutInterval
s into samples.
Adding Samples While Saving a Workout
Open WorkoutDataStore.swift and paste this new method right after save(prancerciseWorkout:completion:)
:
private class func samples(for workout: PrancerciseWorkout) -> [HKSample] { //1. Verify that the energy quantity type is still available to HealthKit. guard let energyQuantityType = HKSampleType.quantityType( forIdentifier: .activeEnergyBurned) else { fatalError("*** Energy Burned Type Not Available ***") } //2. Create a sample for each PrancerciseWorkoutInterval let samples: [HKSample] = workout.intervals.map { interval in let calorieQuantity = HKQuantity(unit: .kilocalorie(), doubleValue: interval.totalEnergyBurned) return HKCumulativeQuantitySeriesSample(type: energyQuantityType, quantity: calorieQuantity, start: interval.start, end: interval.end) } return samples }
Hey, you’ve seen this before! It’s the same thing you did when submitting a body mass index sample in the previous HealthKit tutorial. You’re doing it inside of map
, creating a sample for each PrancerciseWorkoutInterval
associated with your PrancerciseWorkout
.
Now, you’ll make a few adjustments to the code to attach the samples to your workout.
Inside the save(prancerciseWorkout:completion:)
method, replace the following code:
guard let quantityType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) else { completion(false, nil) return } let unit = HKUnit.kilocalorie() let totalEnergyBurned = prancerciseWorkout.totalEnergyBurned let quantity = HKQuantity(unit: unit, doubleValue: totalEnergyBurned) let sample = HKCumulativeQuantitySeriesSample(type: quantityType, quantity: quantity, start: prancerciseWorkout.start, end: prancerciseWorkout.end) //1. Add the sample to the workout builder builder.add([sample]) { (success, error) in guard success else { completion(false, error) return } //2. Finish collection workout data and set the workout end date builder.endCollection(withEnd: prancerciseWorkout.end) { (success, error) in guard success else { completion(false, error) return } //3. Create the workout with the samples added builder.finishWorkout { (_, error) in let success = error == nil completion(success, error) } } }
With this:
let samples = self.samples(for: prancerciseWorkout) builder.add(samples) { (success, error) in guard success else { completion(false, error) return } builder.endCollection(withEnd: prancerciseWorkout.end) { (success, error) in guard success else { completion(false, error) return } builder.finishWorkout { (workout, error) in let success = error == nil completion(success, error) } } }
This code prepares a list of samples using your Prancercise workout. Then, it adds them to the workout builder as you’ve done before.
Build and run the app. Tap on Prancercise Workouts . Then, tap the + button to track a new Prancercise workout. Record a few Prancercise sessions and tap Done to save them to HealthKit as a single Prancercise workout.
Viewing Workout Samples in the Health App
You aren’t going to see anything new in the Prancercise Tracker’s user interface, but there’s loads of data in your Health app to peruse.
Open the Health app . Tap the second tab labeled Health Data , then tap on Activity . You should see a breakdown of your workouts for the day.
You can see very short Prancercise sessions recorded, so Activity says the user has spent one minute exercising. That’s fine. You have already established the relative intensity of the Prancercise regimen, so it ought to be enough physical exertion for a day. :]
Tap Workouts . The next screen will give you a breakdown of your workouts for the day. In your case, you want to see where all those data points came from.
Tap Show All Data . This will take you to a screen that displays all your workouts for the day, along with their source app.
Neat. The RW Logo shows that the workouts came from Prancercise Tracker.
Tap on a workout to view its details. Scroll down to the Workout Samples section, and then tap on the cell displaying the total active energy.
At this point, you should see a list of active energy samples associated with the Prancercise workout you just tracked.
Tap on a sample, and you can see when your short Prancercise session started and finished.
Awesome! You’ve built an app that not only tracks a workout but also tracks interval training within that workout.
Where to Go From Here?
You can download the final project using the Download materials button at the top or bottom of this tutorial.
This HealthKit tutorial has given you some insight into the basic concepts of HealthKit and how to use them in your own apps. To know more about HealthKit, these are the most relevant resources:
- HealthKit Framework Reference .
- WWDC 2018 Session: New Ways to Work with Workouts .
- App Store Review Guidelines related to HealthKit. You need to make sure that your app complies with those guidelines.
After going through those documents and videos, you’ll be ready to dig into some more advanced aspects of HealthKit and add improvements to this app. For instance, you could add new types of samples or workouts, calculate Statistics using HKStatisticsQuery
or observe changes in the store information with HKObserverQuery
.
I hope you enjoyed this HealthKit tutorial and, as always, if you have any questions or comments please join the forum discussion below!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK