Blog

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.

Workout App

 

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.   

Xcode

Health-Kit

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.

WatchKit

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.

storyboard

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.

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

HKAnchoredObjectQueryMostly 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!

Ready to be Unstoppable? Partner with Gorilla Logic, and you can be.

TALK TO OUR SALES TEAM