Recording Workouts in HealthKit

One of the types of data that can be stored in the Apple Health app (or HealthKit, as the developer framework is known), is workout data.

HealthKit is a large framework with many options, so this article will break down a few basic concepts that you need in order to get going with it.

In HealthKit, a single data entry is called a sample, which is how it will be referred to in this article.

Adding the HealthKit Entitlement

When publishing an app that access Health data, you need to add the HealthKit entitlement to the app. You can do so under the Capabilities of your app's target in Xcode.

Checking for HealthKit Availability

When programming HealthKit, the first thing you need to do is to ensure Health data is available on the device.

HealthKit is not available on iPad or iPod Touch (it is also not available on tvOS, but in that case the HealthKit framework isn't even available, so you can't use it anyway).

You can check for the presence of the Health app as follows:

import HealthKit

if HKHealthStore.isHealthDataAvailable() { 
    // HealthKit is available. Use at will.
}
else {
    // Unable to use HealthKit. Fail gracefully to non-Health features
}

Adding HealthKit Capabilities

Most operations in HealthKit involve using an instance of HKHealthStore. There should only be one instance of this class created in your app, so you should either instantiate this object your app delegate, or create a singleton class to manage this.

class AppDelegate: UIResponder, UIApplicationDelegate {
    let healthStore = HKHealthStore()

    // ...
}

You can then access it in future as follows:

let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let healthStore = appDelegate.healthStore

This is somewhat cumbersome, so a single class to help with HealthKit related tasks may be better:

class HealthHelper {
    class var sharedInstance: HealthHelper {
        struct Singleton {
            static let instance = HealthHelper()
        }
        
        return Singleton.instance
    }

    let healthStore = HKHealthStore()
}

This would allow you to access the health store as follows:

let healthStore = HealthHelper.sharedInstance.healthStore

Requesting Permission

Before you can access a user's data in HealthKit, the user needs to grant your app permission to do so.

HealthKit breaks permissions down by data type (such as "workouts", or "weight", or "heart rate"), and within each type there can be read or write access.

  • Read access: You can read data for a given type that has been written by your app or another source (such as when the user manually enters it into the Health app)
  • Write access: You can record new samples. You can also delete samples, but only those recorded by your app.

When requesting permission, you can ask for as many or few permissions as you need in a single request. In this case, we're going to request two permissions: read permission for workouts, and write permission for workouts.

Permissions are requested using the HKHealthStore.requestAuthorizationToShareTypes() method, in which you pass a list of read types and a list of write types.

In the case of workouts, there is a special object type called HKWorkoutType, which can be accessed using HKObjectType.workoutType().

let writeTypes: Set<HKSampleType> = Set([ HKObjectType.workoutType() ])
let readTypes: Set<HKObjectType> = Set([ HKObjectType.workoutType() ])

You can request access to these permissions as follows:

let healthStore = HealthHelper.sharedInstance.healthStore

healthStore.requestAuthorizationToShareTypes(writeTypes, readTypes: readTypes) { (success: Bool, error: NSError?) -> Void in
    // Do something here once finished                
}

HealthKit doesn't actually tell the app if the user granted permission. It is safe to call this method every time you run the app: if a permission has previously been requested, the user will not be prompted again for that permission.

Recording a New Workout

A new sample can be recorded using the HKHealthStore.saveObject() method. Since we're recording a workout, we pass an instance of HKWorkout to this method.

To create a HKWorkout, the minimum amount of data you need is the start and finish times, and the type of activity type (there's a whole bunch available in the HKWorkoutActivityType enum).

For example, to create a running workout that went for an hour, you can use:

let finish = NSDate() // Now
let start = finish.dateByAddingTimeInterval(-3600) // 1 hour ago
        
let workout = HKWorkout(activityType: .Running, startDate: start, endDate: finish)

Sometimes though, the user might pause their workout because they received a phone call or went to do something else. In this case, the actual workout duration is not the full time between the start and finish. HealthKit allows you to record these events also, using the HKWorkoutEvent class.

In the case of the 1 hour workout in the previous code listing, if the user stopped after 5 minutes (300 seconds), then resumed after another 5 minutes (600 seconds since the beginning), you can record these events as follows:

let workoutEvents: [HKWorkoutEvent] = [
    HKWorkoutEvent(type: .Pause, date: startDate.dateByAddingTimeInterval(300)),
    HKWorkoutEvent(type: .Resume, date: startDate.dateByAddingTimeInterval(600))
]

Now you need to alter the creation of the HKWorkout object to use the more complex constructor, which allows you to include workoutEvents:

let workout = HKWorkout(
    activityType: .Running
    startDate: start,
    endDate: end,
    workoutEvents: workoutEvents,
    totalEnergyBurned: nil,
    totalDistance: nil,
    device: nil,
    metadata: nil
)

As you can see, there are other fields you can optionally include. To record the energy burned or distance travelled, pass objects similar to the following:

// 1,000 kilojoules
let totalEnergyBurned = HKQuantity(unit: HKUnit.jouleUnitWithMetricPrefix(.Kilo), doubleValue: 1000)

// 3 KM distance
let totalDistance = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: 3000)

You can also record additional metadata with the workout. There are a number of predefined metadata fields you can use for this (which you can find in HKMetadata.h). Note that each one expects a different type of data with it (in this case, all three accept boolean values).

let metadata: [String: AnyObject] = [
    HKMetadataKeyGroupFitness: true,
    HKMetadataKeyIndoorWorkout: false,
    HKMetadataKeyCoachedWorkout: true
]

To record this extra data, you would use the following to create the workout object:

let workout = HKWorkout(
    activityType: .Running
    startDate: start,
    endDate: end,
    workoutEvents: workoutEvents,
    totalEnergyBurned: totalEnergyBurned,
    totalDistance: totalDistance,
    device: nil,
    metadata: metadata
)

Once you have created the HKWorkout object, pass it to HKHealthStore.saveObject to save it:

healthStore.saveObject(workout) { (success: Bool, error: NSError?) -> Void in

    if success {
        // Workout was successfully saved
    }
    else {
        // Workout was not successfully saved
    }
}

Reading Data

To read workouts that have previously been saved (either by your app or other apps), use the HKHealthStore.executeQuery() method. There are a number of different types of queries that can be supplied (including the ability to automatically detect when new data appears), but in this instance a once-off query will be used.

As noted previously, a single record of data in HealthKit is a sample. To read a list of samples, HKSampleQuery is used.

You can only retrieve a single sample type from a single query. In this case, we want to retrieve workouts:

let sampleType = HKObjectType.workoutType()

Similar to querying a database in CoreData, you can filter the returned data using NSPredicate. To return only records from the past hour, you would use the following:

let startDate = NSDate()
let endDate = start.dateByAddingTimeInterval(-3600)

let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: .StrictStartDate)

In this case, the .StrictStartDate option means only workouts that began between the time range are returned. If a workout started two hours ago but only finished five minutes ago it will not be returned using this predicate.

If you want to use the end date instead, you can use .StrictEndDate. If you want to ensure it started AND finished in the time range, use [ .StrictStartDate, .StrictEndDate ].

You can also sort the returned results using one or more NSSortDescriptor objects. The following would result in workouts being sorted by their end date, with the most recently ended first.

let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)

Additionally, you can limit the number of results returned using the limit parameter. A value of 0 means all records are returned.

let limit = 0

Using these parameters, you can create the query as follows:

let query = HKSampleQuery(
    sampleType: sampleType,
    predicate: predicate,
    limit: limit,
    sortDescriptors: [ sortDescriptor ]) { (query: HKSampleQuery, results: [HKSample]?, error: NSError?) -> Void in

    // Process the results
}

Of course, this has only created the query. To actually run it, use the following:

healthStore.executeQuery(query)

Finally, you need to actually process the results. Since HealthKit queries are run asynchronously, the supplied completion handler provides the query results.

Rather than duplicating the above, the following code lives inside the completion handler block:

var workouts: [HKWorkout] = []

if let results = results {
    for result in results {
        if let workout = result as? HKWorkout {
            // Here's a HKWorkout object
            workouts.append(workout)
        }
    }
}
else {
    // No results were returned, check the error
}

At the end of this, you will be left with an array of HKWorkout objects.

Filtering Data by App

You can check if a sample was created by your app using the sourceRevision property, which is of type HKSourceRevision.

let workout: HKWorkout = workouts[0]

let sourceRevision = workout.sourceRevision

From this value, you can determine the app and its version from the source and version properties. The source property is of type HKSource, while the version value corresponds to the CFBundleVersion value from the app's Info.plist.

To determine if a given sample was recorded by your app, you can compare source to the corresponding HKSource value for your app. You can do so as follows:

let currentSource = HKSource.defaultSource()

if sourceRevision.source == currentSource {
    print("Recorded by this app")
}
else {
    print("Recorded by an app called \(sourceRevision.source.name)")
}

Deleting Data

An app cannot delete Health data recorded by another app. For example, if you manually enter a new workout in the Health app directly, you will be able to read that sample (if the user has given permission), but you cannot delete it.

The previous section showed how to filter records by the current app. This is useful so you know which records can or cannot be deleted.

To delete a sample, pass it to HKHealthStore.deleteObject().

healthStore.deleteObject(workout) { (success: Bool, error: NSError?) -> Void in

    if success {
        // Workout was deleted
    }
}

Similarly, you can delete multiple objects at once by passing an array of HKObject to HKHealthStore.deleteObjects() (so in this case, you would pass [HKWorkout] to delete multiple workouts).

If any of the workouts cannot be deleted in the call to deleteObjects(), then none of them are deleted.

Happy HealthKit hacking!