49

HealthKit Tutorial With Swift: Workouts [FREE]

 5 years ago
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.

uueqiei.png!web

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.

02-Started-Workout-281x500.png

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).

03-Finished-Workout-281x500.png

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:

  1. Start and end time
  2. Duration
  3. 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:

  1. With the sample ready, it’s time to add it to the workout that you’re building.
  2. Here, you finish the collection of workout data and set the end date for the workout.
  3. 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:

  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 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 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.

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 .

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:

  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.

04-Workouts-List-281x500.png

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.

swift-weights-250x250.png

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. :]

swift-cop-stop-police-1-500x500.png

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.

Simulator-Screen-Shot-iPhone-8-Plus-2018-08-23-at-11.55.55-281x500.png

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.

Simulator-Screen-Shot-iPhone-8-Plus-2018-08-23-at-22.32.04-281x500.png

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.

05-Health-App-281x500.png

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.

06-All-Data-1-281x500.png

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.

07-Workout-Details-281x500.png

At this point, you should see a list of active energy samples associated with the Prancercise workout you just tracked.

08-Sample-List-281x500.png

Tap on a sample, and you can see when your short Prancercise session started and finished.

09-Single-Session-281x500.png

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:

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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK