Apple Watch HealthKit Developer Tutorial: How to Build a Workout App
Introduction to Workout Apps
Having a healthier lifestyle is a challenge for many people; most of the time it is because we forget to stand up and move. That is why workout and exercise apps are having a lot of success in the App Store. It is a market that moves millions of dollars a year. In this post, we are going to explain how to build a workout tracker App to improve our health and our quality of life.
Project Description
The goal of the app is to allow users to create workouts like walking and running, select a date and time to schedule those workouts, and receive workout reminders to help them stay on track. Also, we wanted to provide users with a way to track their heart rate (BPM – beats per minute) and the amount of calories they burned while exercising. This is where the Apple Watch sensors come into play.
Technical Details
In this HealthKit / Apple Watch overview, we will walk you through some of the code we used in this project and some of the challenges we faced:
- Brief introduction to HealthKit
- Setting up HealthKit iOS project
- How to request permission and access HealthKit data
- Retrieving data from HealthKit
- Brief introduction to Apple Watch development (watchOS)
- Setting up Apple Watch
- Communication between Apple Watch app and the iPhone app
- HealthKit + Apple Watch
We will be using several Apple tools to build, run and test our app.
Hardware:
- iPhone
- Apple Watch
Software, Language, and Libraries:
- Swift
- HealthKit
- WatchKit
Project Architecture
Since the Apple Watch is still fairly dependent on connection to an iPhone (although less so with the series 3), the iPhone app is in charge of keeping track of the workouts, creating new workouts, starting a workout and displaying one’s progress on the screen. That means we need to store the data and keep the status of the workout in sync with the Apple Watch. The app employs the Apple Watch for quick interactions, such as selecting a workout, and utilizes its heart rate sensor to monitor the user’s heart rate. Below, you will find the workout list screen that keeps track of active, upcoming and past workouts. The data is stored using CoreData, and data is updated in real time for active workouts via communication with the watch.
4 Steps to Set Up a HealthKit Integration Project
Setting up a project that integrates HealthKit is simple with these hands-on steps!
Step 1. Create the iPhone App and the Watch Extension
Open Xcode and create a new project, then enable HealthKit so that you can track the user data.
Like other personal user information (photos, location, etc.), HealthKit requires user permission. After we set up our project and enable HealthKit, we are ready to request access to it. We are going to do so by using HKHealthStore.isHealthDataAvailable().
// Validates if the HealthKit framework has the authorization to read
func authorizeHealthKit() {
if HKHealthStore.isHealthDataAvailable() {
let infoToRead = Set([
HKSampleType.characteristicType(forIdentifier: .biologicalSex)!,
HKSampleType.characteristicType(forIdentifier: .dateOfBirth)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .heartRate)!,
HKSampleType.workoutType()
])
let infoToWrite = Set([
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKObjectType.quantityType(forIdentifier: .heartRate)!,
HKObjectType.workoutType()
])
healthStore.requestAuthorization(toShare: infoToWrite,
read: infoToRead,
completion: { (success, error) in
self.delegate?.workout(manager: self,
didAuthorizeAccess: success,
error: error)
})
} else {
Step 2. Request the user’s authorization by calling the requestAuthorizationToShareTypes:
healthStore.requestAuthorization(toShare: infoToWrite,
read: infoToRead,
completion: { (success, error) in
self.delegate?.workout(manager: self,
Since users can update their authorization status in settings, this value can be checked at any time by calling authorizationStatusForType.
Step 3. Create an instance of HKHealthStore, which is responsible for interacting with the HealthKit data base.
// HKhealthStore
public let healthStore = HKHealthStore()
Step 4. After we create the iOS App, we can continue adding the Watch Extension to the project. In this section, we are going to create the actual watch app, the UI, and the code to collect the heart rate data.
To add a watch app to your existing iOS app project in Xcode, open the project for your iOS app. Choose File > New > Target, and navigate to the watchOS section.
This action generates two folders, AppName WatchKit Extension and AppName WatchKit App, in our project navigator.
The interface for the watch app will take place in the WatchKit app storyboard, and the extension will contain all the code.
Retrieving Data from HealthKit
HealthKit provides push and pull-based queries for retrieving data.
- Push-based Queries: Return a result every time an event is logged.
- Pull-based Queries: Return a result just once, after being initiated.
To successfully execute a query, either push- or pull-based, with HealthKit, you need to follow the three steps below:
- Define a HKQuery object.
- Execute the query on the HKHealthStore object to obtain results.
- Define an HKQuery object with a type matching the result type (such as statistics, samples, cloud-backed data, etc.) and the desired execution method (push or pull).
HKQuery is an abstract class used to find specific data from the HealthKit store.
Table 1, below, explains the most important HKQuery subclasses.
HKQuery | Description | Execution method |
---|---|---|
HKSampleQuery | Immutable query that searches for sample data using a specific filter (time and quantity) and sorted results in a specific order. | Pull (1 event) |
HKStatisticsQuery | Immutable query that executes statistical calculations (i.e. minimum, maximum, average, sum for cumulative quantities, etc) for quantity samples objects, returns | Pull |
HKStatisticsCollectionQuery | Mostly immutable query that executes multiple statistical calculations (i.e. total number of steps for a day, etc) for quantity sample objects. It returns a collection of results. | Pull |
HKObserverQuery | Immutable query that observes and responds to changes to a quantity object (i.e. step count data changes) | Push (generates events until stopped) |
HKAnchoredObjectQuery | Mostly immutable query that uses “anchors – holders” to retrieve the most recent data in the HealthKit store. | Pull |
Adapted from Apple’s documentation regarding HealthKit sample types.
Communication between the Apple Watch App and the iPhone App
Sync Manager
For the communication between the Apple Watch and the iOS App, we need a sync manager on both sides of the project. That means we need to have a sync manager on the watch side and the iPhone side. The way Apple supports communication between the two of them is the WatchConnectivity framework, which has delegate methods to send and receive data in real time (when both apps are in the foreground) or coordinated in the background (when one or both apps are in the background).
This sync manager relies on the WCSession to create a communication session between the iPhone and the watch. This session is the one sending and receiving data. The type of data that we can send is limited to system classes. To send and receive data from one side to the other, we use the following methods (depending on the cases mentioned before).
WatchConnectivity
This framework is available on both iOS and watchOS. It is the mechanism for sending information bidirectionally between the Apple Watch and the iPhone.
import WatchConnectivity
WCSession
The session used for communication between the app and the extension is a WCSession. To get real time updates from the watch, we have to coordinate the sync managers in the iOS app and the watch extension. That way, we can get updates even when the app is not in the foreground.
private let session: WCSession? = WCSession.isSupported() ? WCSession.default() : nil
var validSession: WCSession? {
// Adapted from https://gist.github.com/NatashaTheRobot/6bcbe79afd7e9572edf6
#if os(iOS)
if let session = session, session.isPaired && session.isWatchAppInstalled {
return session
}
#elseif os(watchOS)
return session
#endif
return nil
}
Getting Real-Time Updates
To read the current user’s health data, we created observers for the real-time updates.
One of the challenges we encountered when retrieving heart rate was the definition of which was the best / optimal HKQuery to use. In the end, and after doing some research, we opted to use HKObserverQuery. This query lets us know when the heart rate value has been updated.
// Adapted from https://stackoverflow.com/questions/30556642/healthkit-fetch-data-between-interval
func observerHeartRateSamples() {
let heartRateSampleType = HKObjectType.quantityType(forIdentifier: .heartRate)
if let observerQuery = observerQuery {
healthStore.stop(observerQuery)
}
observerQuery = HKObserverQuery(sampleType: heartRateSampleType!, predicate: nil) { (_, _, error) in
if let error = error {
print("Error: \(error.localizedDescription)")
return
}
self.fetchLatestHeartRateSample { (sample) in
guard let sample = sample else {
return
}
DispatchQueue.main.async {
let heartRate = sample.quantity.doubleValue(for: self.heartRateUnit)
print("Heart Rate Sample: \(heartRate)")
self.updateHeartRate(heartRateValue: heartRate)
}
}
}
healthStore.execute(observerQuery)
}
func fetchLatestHeartRateSample(completionHandler: @escaping (_ sample: HKQuantitySample?) -> Void) {
guard let sampleType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate) else {
completionHandler(nil)
return
}
let predicate = HKQuery.predicateForSamples(withStart: Date.distantPast, end: Date(), options: .strictEndDate)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
let query = HKSampleQuery(sampleType: sampleType,
predicate: predicate,
limit: Int(HKObjectQueryNoLimit),
sortDescriptors: [sortDescriptor]) { (_, results, error) in
if let error = error {
print("Error: \(error.localizedDescription)")
return
}
completionHandler(results?[0] as? HKQuantitySample)
}
healthStore.execute(query)
}
To initialize the HKObserverQuery, we use the method HKObserverQuery(sampleType: HKSampleType, predicate: NSPredicate?, updateHandler: @escaping (HKObserverQuery, HealthKit.HKObserverQueryCompletionHandler, Error?) -> Swift.Void
/*!
@method initWithSampleType:predicate:updateHandler:
@abstract This method installs a handler that is called when a sample type has a new sample added.
@discussion If you have subscribed to background updates you must call the passed completion block
once you have processed data from this notification. Otherwise the system will continue
to notify you of this data.
*/
public init(sampleType: HKSampleType, predicate: NSPredicate?, updateHandler: @escaping (HKObserverQuery, HealthKit.HKObserverQueryCompletionHandler, Error?) -> Swift.Void)
For the sample type, we used the HKQuantityType object corresponding to heart rate (all HKQuantityType objects are subclasses of HKSampleType).
let sampleType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate
You do not need to specify a predicate for the query, because you do not need to filter the events. Every change to the heart rate will be observed.
Regarding the obtention of real time calorie updates during a workout, we triggered an HKAnchoredObjectQuery which uses an “anchor-holder” to retrieve the most recent data from the HealthKit store.
func startQuery(quantityTypeIdentifier: HKQuantityTypeIdentifier) {
let datePredicate = HKQuery.predicateForSamples(withStart: workoutStartDate, end: nil, options: .strictStartDate)
let devicePredicate = HKQuery.predicateForObjects(from: [HKDevice.local()])
let queryPredicate = NSCompoundPredicate(andPredicateWithSubpredicates:[datePredicate, devicePredicate])
let updateHandler: ((HKAnchoredObjectQuery, [HKSample]?,
[HKDeletedObject]?,
HKQueryAnchor?,
Error?) -> Void) = { query,
samples,
deletedObjects,
queryAnchor,
error in
self.process(samples: samples, quantityTypeIdentifier: quantityTypeIdentifier)
}
let query = HKAnchoredObjectQuery(type: HKObjectType.quantityType(forIdentifier: quantityTypeIdentifier)!,
predicate: queryPredicate,
anchor: nil,
limit: HKObjectQueryNoLimit,
resultsHandler: updateHandler)
query.updateHandler = updateHandler
healthStore.execute(query)
activeDataQueries.append(query)
}
To initialize the HKAnchoredObjectQuery, we use the method HKAnchoredObjectQuery (type: HKSampleType, predicate: NSPredicate?, anchor: HKQueryAnchor?, limit: Int, resultsHandler handler: @escaping (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Swift.Void)
/*!
@method initWithType:predicate:anchor:limit:resultsHandler:
@abstract Returns a query that will retrieve HKSamples and HKDeletedObjects matching the given predicate that are
newer than the given anchor.
@discussion If no updateHandler is set on the query, the query will automatically stop after calling resultsHandler.
Otherwise, the query continues to run and call updateHandler as samples matching the predicate are
created or deleted.
@param type The type of sample to retrieve.
@param predicate The predicate which samples should match.
@param anchor The anchor which was returned by a previous HKAnchoredObjectQuery result or update
handler. Pass nil when querying for the first time.
@param limit The maximum number of samples and deleted objects to return. Pass HKObjectQueryNoLimit
for no limit.
@param handler The block to invoke with results when the query has finished finding.
*/
@available(watchOS 2.0, *)
public init(type: HKSampleType, predicate: NSPredicate?, anchor: HKQueryAnchor?, limit: Int, resultsHandler handler: @escaping (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Swift.Void)
For the quantityTypeIdentifier, we used the HKQuantityTypeIdentifier.activeEnergyBurned object corresponding to cumulative energy / calories.
startQuery(quantityTypeIdentifier: HKQuantityTypeIdentifier.activeEnergyBurned)
Important things to Keep in Mind:
HealthKit: This is a shared “data base” of knowledge across the system, which means we have access to all the data recorded in it. As a result, we have a lot of data to play with. It is also important to keep in mind that the iPhone and the Apple Watch have their own data, and iOS is the one in charge of synchronizing that data.
WatchKit: Building an elaborate user interface for the Apple Watch can be a very tricky and complex task to accomplish since the UI for the watch is a stack base UI. A stack base UI uses stacks to accommodate the controls on the screen and can be frustrating at first if you don’t have the expertise. This is much more limited compared to regular xib / Storyboards UI that iOS developers are used to working with.
Sync: Keeping the data updated on both sides of the app (iPhone and Apple Watch) is not as complicated as it sounds, but it is confusing at first. Once you understand the sync flow, though, the data transfer is easy to manage.
Interested in more “How To” posts? Check out our IoT Success Series!