Working Out with HealthKit

The sensors we looked at accessing in the previous part of this chapter, such as the accelerometer, are great at providing data about the device, but the Apple Watch has another sensor available to us that’s only available on the watch: the heart rate sensor on the back of the device. This sensor, combined with the impressive water resistance of the watch, makes it an amazing companion for working out, whether you’re a weekend warrior or professional athlete. Let’s add support for this sensor to TapALap. You’ll use the watch to save a workout session to HealthKit, which will allow you not only to access heart rate and calorie burn data but also to save the data back to the user’s phone and contribute to his daily Activity rings!

What Is HealthKit?

One of Apple’s main focuses with the Apple Watch is health. The device focuses much of its capability on its wearers’ personal health: periodically sampling their heart rate, reminding users to stand periodically throughout the day, and encouraging them to make healthy habits like exercising for 30 minutes every day. To do these things, the watch needs a place to store the users’ health data: heart rate readings, hours in which they’ve stood, and the exercise they’ve logged. These are all separate apps, too; the Workout app tracks workouts, the Activity app tracks standing, and so on. Wouldn’t it be nice to share that data between apps?

HealthKit is Apple’s answer to this set of needs. Instead of every health-related app creating its own system to store, synchronize, and share data, quickly leading to dozens of conflicting standards, Apple offers HealthKit as a common system framework to store personal health data. Apps can save health data to HealthKit, read data from HealthKit—even from other apps—and trust that the data is safe and secure on the user’s device. Apps can use as much or as little of HealthKit as they want, but even an app that just saves user data to it makes the entire system more valuable to the user. Not only does HealthKit help you build your app by giving you a head start on your data store, but it can also serve to market your app. A user who’s heavily invested in using HealthKit, when given the choice between two apps to do the same thing, one of which supports HealthKit and one of which doesn’t, will choose the HealthKit app just to get his data in one place. Supporting HealthKit is a feature unto itself, but as you’ll see in this chapter, it’s also very handy when developing a workout app!

Whereas HealthKit is the interface we developers use to store health-related data, our users will use Apple’s Health app on their iPhone to see a dashboard of their health data. In addition to the Health app, there’s an Activity app to track the user’s Apple Watch--specific activity: workout minutes, calories burned, and hours in which the user has stood. As you’ll see later, supporting HealthKit will add your app to these apps, presenting your data in context for the user next to theirs. For more information on the Health and Activity apps, consult Apple’s websites, both for iOS[9] and for watchOS.[10]

Asking for Permission

One of the biggest features of HealthKit is privacy. It’s your users’ most personal data, so Apple goes to great lengths to protect it. To that end, you can’t just start writing code to talk to HealthKit; you need to ask the user if it’s OK to proceed. The appropriate time to start saving data is when the user taps Start Run, so you’ll ask the user for permission in the willActivate method of your RunTimerInterfaceController. First, you’ll need to import the HealthKit framework to have access to the classes and methods you’ll need:

 import​ ​HealthKit

You’ll need a lazy HKHealthStore to facilitate communication with HealthKit:

 lazy​ ​var​ healthStore = ​HKHealthStore​()

With that done, you can add a new method to your class, askForHealthKitPermission, in which you’ll actually ask the user for the permission you need. You’ll call it from willActivate:

 override​ ​func​ willActivate() {
 super​.willActivate()
 
» askForHealthKitPermission()
 
 // Don’t delete the rest of this method—it’s just removed to save space!
 
 }
 
 func​ askForHealthKitPermission() {
 guard​ ​HKHealthStore​.isHealthDataAvailable() ​else​ { ​return​ }
 
 guard​ ​let​ caloriesBurnedType = ​HKObjectType​.quantityTypeForIdentifier(
 HKQuantityTypeIdentifierActiveEnergyBurned​),
  distanceRunType = ​HKObjectType​.quantityTypeForIdentifier(
 HKQuantityTypeIdentifierDistanceWalkingRunning​) ​else​ { ​return​ }
 
 let​ shareTypes = ​Set​([​HKObjectType​.workoutType(), caloriesBurnedType,
  distanceRunType])
 
 let​ readTypes = ​Set​([caloriesBurnedType])
 
  healthStore.requestAuthorizationToShareTypes(shareTypes,
  readTypes: readTypes,
  completion: { (success: ​Bool​, error: ​NSError​?) ​in
 if​ ​let​ error = error ​where​ !success {
  print(​"Error authorizing HealthKit: "​ +
  error.localizedDescription + ​"."​)
  }
 else​ ​if​ !success {
  print(​"You didn’t authorize HealthKit. Open the Health app on "
  + ​"your iPhone to give TapALap the correct permissions."​)
  }
  })
 }

Before you do anything else, you call isHealthDataAvailable to make sure that you can use HealthKit at all. If not, you simply return and do nothing. Next, you need to use some data types from HealthKit, but the methods to obtain them return optional types, so you use another guard statement to ensure they exist. You need data types for calories burned and the distance the user has run. Data types in hand, you need to tell HealthKit which data types you’re reading and which you’re writing—or, in the parlance of HealthKit, sharing. You want to share three pieces of data: workouts themselves, calories burned (which you’ll associate with the workout later), and distance run. You put those in an array and then turn it into a Set, which is what HealthKit is expecting, called shareTypes. You need to do the same for the types you’ll be reading from HealthKit. For now, you just need to read the calories burned, so it’s the lone data type in your readTypes set. Armed with both Sets of types, you can call requestAuthorizationToShareTypes(_:readTypes:completion:). If this is the first time the app has run, the user will see an authentication dialog, allowing him to grant the app access.

The user won’t actually give you permission on the watch, however. Instead, you need to open the iOS app delegate, where you’re given a chance to perform the authorization. In the TapALap group, open AppDelegate.swift, import HealthKit, and add the following method:

 import​ ​HealthKit
 func​ applicationShouldRequestHealthAuthorization(application: ​UIApplication​) {
 let​ store = ​HKHealthStore​()
 
  store.handleAuthorizationForExtensionWithCompletion { success, error ​in
 if​ ​let​ error = error ​where​ !success {
  print(​"HealthKit authorization failed. Error: "
  + error.localizedDescription)
  }
  }
 }

Now, when the extension requests HealthKit access, it’ll get permission from the user by opening the iOS app as seen in the figures.

images/TapALapCombined.png

When the user taps Allow, your app will be granted access! For this to work, you just need to ensure that the WatchKit extension and iOS app have the HealthKit entitlement enabled in their app IDs.

With your HealthKit authorization complete, begin collecting your data.

Creating a Workout Session

At the core of your interaction with HealthKit is an HKWorkoutSession. Starting a workout session does a couple of things for you. Most importantly, it activates the heart rate sensor. Obviously, this will have an impact on your users’ battery life, so you should only use a workout session for when the user is actually working out. Trying to use a workout session to see the user’s heart rate while he sleeps at night, for example, would result in depleted batteries and sad users. Another benefit of creating a workout session, aside from the heart rate data, is that the watch will keep your app in the foreground automatically. The watch’s screen will still turn off to conserve power, but when the user raises his wrist, the app with the foreground workout session will be visible instead of the watch face.

Joe asks:
Joe asks:
What is an entitlement, and how do I add one for HealthKit?

Much like a provisioning profile declares who can digitally sign your code and on which devices it can run, entitlements declare what the app is allowed to do. One of the most common entitlements is called get-task-allow, which allows the debugger to attach to release builds. Entitlements are specified in a property list file (typically Entitlements.plist) in your app, with the name of the entitlement as a string key and the associated Boolean value determining if your app supports that entitlement.

For any app to access HealthKit, add the HealthKit entitlement, com.apple.developer.healthkit, to its app ID. The easiest way to do this is using Xcode. Select the project settings, and then navigate to the Capabilities tab with TapALap WatchKit Extension selected. Click the switch next to HealthKit to enable it…most of the time.

images/EnableHealthKitCapability.png

There are situations where the switch won’t work to automatically enable the HealthKit entitlement. If that happens, you’ll need to head to the developer center at https://developer.apple.com and enable it manually. Navigate to the Certificates, Identifiers, and Profiles section; then click App IDs. Select your app ID; then check the box next to HealthKit, as shown here:

images/AddHealthKitEntitlementManually.png

If you’re doing this manually, you’ll need to regenerate your provisioning profiles to include the new entitlement.

As you transition to the Run Timer interface controller and begin the run, you’ll also start a workout session. To begin, you need to declare that the RunTimerInterfaceController conforms to the HKWorkoutSessionDelegate protocol. This will allow you to receive messages from the workout session as it’s in progress:

 class​ ​RunTimerInterfaceController​: ​WKInterfaceController​, ​HKWorkoutSessionDelegate​ {

Now it’s time to actually start your workout session. You’ll make a new method, startWorkoutSession, in the interface controller:

 var​ currentWorkoutSession: ​HKWorkoutSession​?
 
 func​ startWorkoutSession() {
 let​ session = ​HKWorkoutSession​(activityType: .​Running​,
  locationType: .​Indoor​)
 
  session.delegate = ​self
 
  currentWorkoutSession = session
 
  healthStore.startWorkoutSession(session)
 }

This method is a simple matter of creating a session, setting the interface controller as its delegate, and telling the health store to start the session. You’ll use the currentWorkoutSession variable to keep track of it elsewhere. You provide a couple of options to the session, indicating that the workout is a run and it’s taking place inside; then you’re good to go. For it to be the session’s delegate, however, you have to implement a pair of methods. First, and most simply, you have to implement workoutSession(_:didFailWithError:) to handle failure of the session:

 func​ workoutSession(workoutSession: ​HKWorkoutSession​,
  didFailWithError error: ​NSError​) {
  print(​"Workout session error: ​​(​error.localizedDescription​)​​"​)
 }

In this case, the workout session failing isn’t the end of the world—you can still finish your run, add more laps, and so on, so you’ll fail gracefully here, merely logging the error and continuing. Next, you’ll need to implement workoutSession(_:didChangeToState:fromState:date:) to handle the workout ending:

 func​ workoutSession(workoutSession: ​HKWorkoutSession​,
  didChangeToState toState: ​HKWorkoutSessionState​,
  fromState: ​HKWorkoutSessionState​,
  date: ​NSDate​) {
 // If the workout ends before we call endRun(), end the run.
 if​ ​let​ _ = lapTimes, _ = startDate ​where​ toState == .​Ended​ {
  endRun()
  }

In this method, if the workout is changing to the Ended state, you call endRun. Ending the workout session is as easy as starting it, so let’s do that next.

Ending the Workout

With your delegate methods implemented, go to the endRun method and end your workout session:

 func​ endRun() {
»if​ ​let​ session = currentWorkoutSession {
» healthStore.endWorkoutSession(session)
» currentWorkoutSession = ​nil
» }
 
 let​ names = [​"GoRunning"​, ​"RunLog"​]
 
 let​ contexts: [​AnyObject​]?
 
 if​ ​let​ lapTimes = lapTimes, startDate = startDate {
 let​ distance = track.lapDistance * ​Double​(lapTimes.count)
 
 let​ run = ​Run​(distance: distance,
  laps: lapTimes,
  startDate: startDate)
 
 let​ userDefaults = ​NSUserDefaults​.standardUserDefaults()
  userDefaults.removeObjectForKey(​"LapTimes"​)
  userDefaults.removeObjectForKey(​"StartDate"​)
  userDefaults.synchronize()
 
  contexts = [​NSNull​(), run]
  }
 else​ {
  contexts = ​nil
  }
 
 WKInterfaceController​.reloadRootControllersWithNames(names,
  contexts: contexts)
 }

The lines at the top are enough to end the workout session. Easy, huh? Calling endWorkoutSession(_:) on the HKHealthStore object is enough to save your data, turn off the heart rate sensor, and allow other apps into the foreground of the watch. You’re not quite finished yet, however; to allow your app to contribute to the user’s Activity rings, you’ll need to create an HKWorkout object to store all of this data in HealthKit.

Contributing to Activity Rings

So far what you’ve done has been sufficient for starting the heart rate monitor and keeping your app in the foreground. To make a workout app really shine, you need to use the data you’re gathering as the user works out to contribute to his Activity rings in the Activity app on watchOS. These rings serve as motivation to work out, so by contributing to them, they will also serve as motivation to use your app! All you need to do is to save the workout to the watch when you’re finished. You’ll add a new method, saveWorkoutSession(_:), and call it after you end the workout session in endRun:

 if​ ​let​ session = currentWorkoutSession {
  healthStore.endWorkoutSession(session)
  saveWorkoutSession(session)
  currentWorkoutSession = ​nil
 }

The saveWorkoutSession(_:) method is responsible for creating an HKWorkout instance and saving it to HealthKit:

 var​ currentCaloriesBurned = ​HKQuantity​(unit: ​HKUnit​.kilocalorieUnit(),
  doubleValue: 0.0)
 
 var​ calorieSamples: [​HKQuantitySample​] = []
 
 func​ saveWorkoutSession(session: ​HKWorkoutSession​) {
guard​ ​let​ caloriesBurnedType = ​HKObjectType​.quantityTypeForIdentifier(
 HKQuantityTypeIdentifierActiveEnergyBurned​),
  distanceRunType = ​HKObjectType​.quantityTypeForIdentifier(
 HKQuantityTypeIdentifierDistanceWalkingRunning​) ​else​ { ​return​ }
 
 // Ensure we have permission to save all types
for​ type ​in​ [caloriesBurnedType, distanceRunType, .workoutType()] {
 if​ healthStore.authorizationStatusForType(type) != .​SharingAuthorized​ {
 return
  }
  }
 
guard​ ​let​ startDate = startDate, lapTimes = lapTimes
 where​ !lapTimes.isEmpty
 else​ { ​return​ }
 
let​ endDate = startDate.dateByAddingTimeInterval(
  lapTimes.reduce(0, combine: +))
 
 let​ lapDistance = track.lapDistance
 
let​ distanceRun = ​HKQuantity​(unit: ​HKUnit​.meterUnit(),
  doubleValue: lapDistance * ​Double​(lapTimes.count))
 
let​ workout = ​HKWorkout​(activityType: ​HKWorkoutActivityType​.​Running​,
  startDate: startDate,
  endDate: endDate,
  duration: endDate.timeIntervalSinceDate(startDate),
  totalEnergyBurned: currentCaloriesBurned,
  totalDistance: distanceRun,
  metadata: ​nil​)
 
let​ finalCalorieSamples = calorieSamples
 
healthStore.saveObject(workout) { [​unowned​ ​self​] success, error ​in
 if​ ​let​ error = error ​where​ !success {
  print(​"Failed to save the workout: "
  + error.localizedDescription)
 return
  }
 
 if​ success && finalCalorieSamples.count > 0 {
 // Associate the accumulated samples with the workout.
self​.healthStore.addSamples(finalCalorieSamples,
  toWorkout: workout) { success, error ​in
 if​ ​let​ error = error ​where​ !success {
  print(​"Failed to add calorie samples to the workout:"
  + error.localizedDescription)
  }
  }
  }
 
 if​ success {
 let​ lapDistanceQuantity = ​HKQuantity​(unit: ​HKUnit​.meterUnit(),
  doubleValue: lapDistance)
 
 var​ lapStartDate = startDate
 
 var​ samples: [​HKSample​] = []
 
for​ i ​in​ lapTimes.startIndex ..< lapTimes.endIndex {
 let​ lapTime = lapTimes[i]
 let​ lapEndDate = lapStartDate.dateByAddingTimeInterval(lapTime)
 
let​ sample = ​HKQuantitySample​(type: distanceRunType,
  quantity: lapDistanceQuantity,
  startDate: lapStartDate,
  endDate: lapEndDate,
  device: ​HKDevice​.localDevice(),
  metadata: ​nil​)
 
  samples.append(sample)
 
  lapStartDate = lapEndDate
  }
 
self​.healthStore.addSamples(samples, toWorkout: workout)
  { success, error ​in
 if​ ​let​ error = error ​where​ !success {
  print(​"Failed to add distance samples to the workout:"
  + error.localizedDescription)
  }
  }
  }
  }
 }

There’s a lot going on in this code sample. Let’s go through it step-by-step:

First, you need to create the required type objects that you’ll use in HealthKit. Since these are optional values, you’ll just exit early if you can’t get them. Assuming you can, you should also have received permission to share them earlier when you asked the user for permission. You check each permission (at ​​) and exit early if any are incorrect.

You need to get some required information about the workout, its start and end date. Here you get the array of lap times (ensuring that there’s at least one lap), and from there you can compute the end date (at ​​).

Since every lap is the same distance, you can multiply the track length by the number of laps to create the next value you’ll need, an HKQuantity representing the number of meters the user ran.

Finally, you’re ready to create your HKWorkout. Its initializer takes all of the data you computed and returns the workout object itself.

Now that you have a workout, you save it to your HKHealthStore. You aren’t finished yet, because you still need to save the calorie data to it. You pass a closure to saveObject(_:completion:) that’s called when the workout is saved and it’s safe for you to add samples to it, so before you call the method (at ​​) you save a copy of the data into a local variable, ensuring that it won’t be modified before the closure finishes running.

Assuming the workout saved successfully, there are two sets of samples to add. First, you add the calories burned samples to the workout.

Next, you add samples for how far the user ran. You know the distance of each lap, so you enumerate the lap times, creating HKQuantitySample objects (at ​​) for each lap. The more you can give to HealthKit, the better.

Finally, you take these distance samples and add them to the workout. Having added all of the data, you’re finished!

The steps we covered create the workout, save associated data to it, and store it in HealthKit. This is great for your users—this data can be shared with all kinds of apps, from weight loss apps to other running apps. Your users might want to use your app on rainy days where they run at the gym but use other apps for running outside, for instance. By implementing HealthKit, you make it easy to add your app to their existing workout regimen or create an entirely new one around it!

You may have noticed that the first two lines of the code sample were declaring variables that you read but never wrote to. You actually aren’t quite finished; you created an empty array to store calorie burn samples and created your running “calories burned” total at 0, but you don’t read that data anywhere. Next, we’ll cover reading that data as it comes in from HealthKit so that you can save the information with the workout—and display it to your users onscreen!

Reading Calorie Burn Data

images/TapALap-CreateCaloriesBurnedLabel.png

We know that starting a workout session on the watch will activate the heart rate sensor and start collecting data. What good is that if we don’t use it? One of the coolest things it does is help to provide more accurate data during exercise. To that end, let’s display the user’s current calories burned on the Run Timer screen and also save the samples with the workout so they show up in the user’s Activity rings.

First, open the TapALap watch app’s Interface.storyboard and add a new label to the Run Timer interface controller with the text “Calories Burned:”, as shown in the figure.

This label will need to fit the calories as well, so set its Minimum Font Scale to 0.5. Next, create a new @IBOutlet declaration in RunTimerInterfaceController.swift:

 @IBOutlet​ ​weak​ ​var​ caloriesBurnedLabel: ​WKInterfaceLabel​!

Finally, return to the storyboard and link them up. Now you’re ready to start collecting data! You’ll create a HealthKit query to instruct HealthKit to continuously deliver calorie data to you, rather than you having to continually ask it for updates. The right time to start this query is when the workout transitions to the running state. Add an else clause to the if in workoutSession(_:didChangeToState:fromState:date:) to handle this case:

 func​ workoutSession(workoutSession: ​HKWorkoutSession​,
  didChangeToState toState: ​HKWorkoutSessionState​,
  fromState: ​HKWorkoutSessionState​,
  date: ​NSDate​) {
 // If the workout ends before we call endRun(), end the run.
 if​ ​let​ _ = lapTimes, _ = startDate ​where​ toState == .​Ended​ {
  endRun()
  }
 else​ ​if​ toState == .​Running​ {
  beginCalorieQuery()
  }
 }

This code calls the beginCalorieQuery method, which you haven’t written yet. Let’s do that next:

1: var​ currentCaloriesBurnedQuery: ​HKQuery​?
func​ beginCalorieQuery() {
guard​ ​let​ calorieType = ​HKObjectType
5:  .quantityTypeForIdentifier(​HKQuantityTypeIdentifierActiveEnergyBurned​)
else​ { ​return​ }
let​ calorieUnit = ​HKUnit​.kilocalorieUnit()
10: guard​ ​let​ startDate = startDate ​else​ { ​return​ }
let​ datePredicate = ​HKQuery​.predicateForSamplesWithStartDate(startDate,
endDate: ​nil​, options: .​None​)
15: let​ processCalorieSamplesFromQuery = { (query: ​HKAnchoredObjectQuery​,
samples: [​HKSample​]?,
deletedObjects: [​HKDeletedObject​]?,
anchor: ​HKQueryAnchor​?,
error: ​NSError​?) -> ​Void​ ​in
20: 
guard​ ​let​ samples = samples ​as?​ [​HKQuantitySample​] ​else​ { ​return​ }
let​ newSamples = samples.map { originalSample ​in
HKQuantitySample​(type: calorieType,
25:  quantity: originalSample.quantity,
startDate: originalSample.startDate,
endDate: originalSample.endDate)
}
30: NSOperationQueue​.mainQueue().addOperationWithBlock { [​weak​ ​self​] ​in
guard​ ​let​ initialCaloriesBurned = ​self​?.currentCaloriesBurned
.doubleValueForUnit(calorieUnit) ​else​ { ​return​ }
let​ newCaloriesBurned = samples.reduce(initialCaloriesBurned) {
35:  $0 + $1.quantity.doubleValueForUnit(calorieUnit)
}
self​?.currentCaloriesBurned = ​HKQuantity​(unit: calorieUnit,
doubleValue: newCaloriesBurned)
40: 
self​?.caloriesBurnedLabel
.setText(​"Calories Burned: ​​(​newCaloriesBurned​)​​"​)
self​?.calorieSamples += newSamples
45:  }
}
let​ query = ​HKAnchoredObjectQuery​(type: calorieType,
predicate: datePredicate,
50:  anchor: ​nil​,
limit: ​Int​(​HKObjectQueryNoLimit​),
resultsHandler: processCalorieSamplesFromQuery)
query.updateHandler = processCalorieSamplesFromQuery
55: 
healthStore.executeQuery(query)
currentCaloriesBurnedQuery = query
}

You start on line 1 by creating a variable to store your query in. You’ll need to reference this later when it’s time to stop the query. Next, after getting your prerequisite data on lines 4 through 10, you create a predicate on line 12 that you’ll use to filter the data. The predicate will restrict the search results to just those that came in after the workout began—otherwise you’d start processing unrelated items! Next, on line 15, you create a closure called processCalorieSamplesFromQuery. You’ll use this closure in two places to process results. The samples parameter to the closure is an array of calorie burn samples. As they come in, you create duplicates on line 23. This might seem weird, but it’s necessary to get the proper attribution—since your app created the new samples, your app will “own” them and show up next to them in the Health app on the user’s phone.

Next, on line 30, you wrap the rest of the code in a closure that gets executed on the main queue. Since you’re going to be updating some more variables and the UI from here, you want to make sure nothing happens out of order. You use the [weak self] capture list to avoid the closure taking ownership of self and causing a reference cycle. If the interface controller is deallocated before the query stops processing, self will be nil when this closure executes and nothing will happen.

Inside the closure, you first get the current calories burned and save it as initialCaloriesBurned. On line 34, use the reduce method to add all of the samples’ data to initialCaloriesBurned. After reducing your calories, create a new HKQuantity out of the new number and save it off. You update your new label with the new amount of calories burned; then on line 44 add the samples to your array of samples, which is what you’ll save to the workout when it’s saved.

After the closure, on line 48, it’s time to create the query. You pass processCalorieSamplesFromQuery in as its resultsHandler and also set it as the query’s updateHandler. The former is called as soon as the query starts with all of the existing data, and the latter is called whenever new data arrives. Since you need to do the same processing for existing and new data, the same closure works in each case. Finally, you instruct your healthStore to start the query and save it as your currentCaloriesBurnedQuery. All that’s left is to stop it when you’ve finished.

To end the query, call stopQuery(_:) on your healthStore. Do this inside endRun:

 func​ endRun() {
 if​ ​let​ query = currentCaloriesBurnedQuery {
  healthStore.stopQuery(query)
  currentCaloriesBurnedQuery = ​nil
  }
 
 
 // Don’t delete the rest of this method, it’s merely removed to save space!
 
 }
images/TapALap-iPhoneActivityApp.png

And with that, you can start a run! You’ll notice the calories updating live as you run, and when you end the run, your watch will automatically sync the data with the phone. TapALap will even appear in the iPhone’s Activity app in the Move section, as shown here!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.87.38