RT Blog
Programming

Importing Data into HealthKit

This post carries on from Importing File Data, where we looked at how to import a GPX file, and parse it into useable objects. If you’ve not read the first part I would recommend doing so before continuing.


Recap

For a quick recap, we ended the last post with an array of Workout objects. The Workout objects consist of a name, start date and an array of track point data. Again, all this information comes from the GPX file we imported in the previous post.

struct Workout {
  
  let name: String
  let startDate: Date
  let trackpoints: [TrackPoint]
}

struct TrackPoint {
  
  let longitude: Double
  let latitude: Double
  let elevation: Double
  let timeStamp: Date
  let heartRate: Double
  let cadence: Int
}

Permissions

Before we can read, or in our case, write to HealthKit, we need to request authorisation to do so. This requires us to call requestAuthorization, and pass in the values which we want permissions for. Below is the overlay iOS presents when you request permissions.

Health Kit permissions overlay

In this example, we have asked to write data for four different pieces of data.

Heart Rate – This is pretty self explanatory, we want to write the heart rate data to a workout so we have to request permission for that.

Walking + Running Distance – Again this very straightforward. This permission allows us to write the total distance for a running or walking activity.

Workout Route – To store location data, we need to enable this permission. It allows us to store the longitude and latitude of a workout, which is what apps use to draw a map of the route.

Workouts – Finally the workouts permissions allows us to save the type of workout, whether it be running, cycling, swimming etc.

It is very simple to request access, all we need to do it call requestAuthorization on an instance of HKHealthStore for example.

import HealthKit

// First check that health data is available for this device.

guard HKHealthStore.isHealthDataAvailable() else {
      completion(false, HealthkitSetupError.notAvailableOnDevice)
      return
    }

// We then create an array of health kit sample types that we wish to ask for permission to write to.   

let healthKitTypesToWrite: Set<HKSampleType> = [
      HKObjectType.quantityType(forIdentifier: .heartRate)!,
      HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!,
      HKSeriesType.workoutRoute(),
      HKObjectType.workoutType()
    ]

// All that is left to do now is to instantiate an instance of HKHealthStore and call requestAuthorization

   HKHealthStore().requestAuthorization(toShare: healthKitTypesToWrite, read: nil) { (success, error) in
      DispatchQueue.main.async {
        // Handle Completion
      }
    }

One thing to note. The completion block is returned on the background queue, so you will need to dispatch to the main thread once ready to update the UI.

We now have all the permissions in place to start writing to Health Kit. All that is left to do is to convert our model objects to HealthKit readable objects, and add them to the health store.


HKWorkout

To save workouts to HealthKit we first need to convert our Workout objects into a HealthKit object called HKWorkout. HKWorkout contains lots of information about the workout, however this is were it gets a bit confusing. Certain types of data, such as heart rate are stored as sample data (HKSample) in the HealthKit store, and then linked to a workout after. The workout route also needs to created separately, and then linked to a workout using HKWorkoutRouteBuilder, which I will pick up later; but for now lets first create our HKWorkout and add it to the store.

First, to create the HKWorkout instance we need to use the initialiser which best suits our example. Annoyingly Apple have not added any default parameters to the HKWorkout initialisers so instead of only passing in the data we want, we just have to pass nil into the values we don’t need.

// This would be the workout we created by parsing the GPX file
let workout: Workout

// Grab the start date from the workout, and the end date from the timestamp of the last trackpoint.
let startDate = workout.startDate
let finishDate = workout.trackpoints.last!.timeStamp

// The distance is a bit different as we need to calculate this from the trackpoints. I will pick up on how we calcuate the total distance later.
let totalDistance = HKQuantity(unit: HKUnit.meter(), doubleValue: calculatedTotalDistance)

let hkworkout = HKWorkout(activityType: .running, start: startDate, end: finishTime, workoutEvents: nil, totalEnergyBurned: nil, totalDistance:  totalDistance, device: nil, metadata: nil)

Once we have the startDate, endDate and totalDistance, we can construct an instance of HKWorkout using the above initialiser. Notice we have to provide an activity type. This data is not part of a GPX file so some form of user input is required here. For my example I bring up an action sheet to ask the user what activity they were doing e.g run, bike, swim etc, and this is how I would populate the activityType within the initialiser. For this example I have just set it to running.

Once the workout is initialised, we can then add it to HealthKit. Like with the authorisation, all we have to do is call a function on HKHealthStore. In this case it’s just save(:), whilst passing in the workout we created before.

 HKHealthStore().save(hkworkout) { (finished, error) in
  // Handle finished and error states. We will pick this up later when we will be adding our sample data.
                                 }

It’s that simple. Once your workout is saved it will appear within HealthKit as well as the Activity app.


Adding Sample Data

We mentioned before, that not all data is stored in HKWorkout, and some data is linked separately. In our case, we need to create samples and location data for both the heart rate and route information. It is in this logic where will also calculate the total distance.

To calculate the total distance, as well as create heart rate and location data for all our track points, we will need to loop through all the track points and create individual sample data for each one. I will show you how I achieved this in the code below.

// This would be the workout we created by parsing the GPX file
let workout: Workout

// Samples will hold a reference to all the sample data that we will later link to our workout.
var samples: [HKQuantitySample] = []

workout.trackpoints.forEach({ (trackpoint) in
       
// To calcualte the heart rate we first set up the HKQuantity to tell the sample what unit of measurmant this piece of data is.                            
        let heartRateQuantity = HKQuantity(unit: HKUnit.count().unitDivided(by: HKUnit.minute()), doubleValue: trackpoint.heartRate)
                             
// We then get the quantity type for heart rate.             
        let heartRateType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!
                             
// Finally create the sample data by passing in the quantity and the type. Notice how the start and end date are the same. This is because our track points are calculated at a single point of time, and the data we get is for that time only.                         
        let heartRateSample = HKQuantitySample(type: heartRateType, quantity: heartRateQuantity, start: trackpoint.timeStamp, end: trackpoint.timeStamp)

// The sample then gets added to our sample array.           
        samples.append(heartRateSample)
       

This will collect heart rate samples for all the track point data. Before we link them to our workout, we first need to create the location and distance data. To do this we still need to be in the forEach loop above. So just below the code that creates the heart rate samples we need to add some extra code.

var samples: [HKQuantitySample] = []

// We create a routeBuilder object which is an instance of HKWorkoutRouteBuilder. This allows us to create a route from CLLocation data which can then be linked to a workout.  
let routeBuilder = HKWorkoutRouteBuilder(healthStore: HKHealthStore(), device: nil)

workout.trackpoints.forEach({ (trackpoint) in
                             
….

// We first create the location data by instantiating a CLLocation object by passing in the relevant longitude, latitude, elevation and time data from the track point.                               
        let location = CLLocation(coordinate: CLLocationCoordinate2D(latitude:  trackpoint.latitude, longitude: trackpoint.longitude), altitude: trackpoint.elevation, horizontalAccuracy: -1, verticalAccuracy: -1, timestamp: trackpoint.timeStamp)

// Finally insert the location data to the routeBuilder. We can either collect all the location data and add the array at the end, but for simplicity I'm doing it on the individual track points inside the loop.                          
        routeBuilder.insertRouteData([location], completion: { (finish, error) in 

// Handle the completion
})
                             

We now have all the location data ready to be linked to the workout, we just need to create the samples for the total distance. So again inside the forEach loop we need to add some more code.

var samples: [HKQuantitySample] = []
let routeBuilder = HKWorkoutRouteBuilder(healthStore: HKHealthStore(), device: nil)

// We need to create a reference to store the previous location and previous timestamp, so we can work out the distance between the two.
var previousLocation: CLLocation?
var previousTimeStamp: Date?

// Also need a totalDistance variable to keep track of the total distance. This is the total distance which we added to the HKWorkout when we initalised it. 
var totalDistance: Double = 0

workout.trackpoints.forEach({ (trackpoint) in
                             
….

// We first unwrap the previous location and timestamp. If they are nil then we don't create any sample data.                             
if let previousLocation = previousLocation, let previousTimeStamp = previousTimeStamp {

    // Calculate the distance using the previous location and the current one.
    let distance = location.distance(from: previousLocation)
    
    // Update global distance.
    totalDistance += distance
   
    // Create the quantity, which for this sample is in meters.  
    let quantity = HKQuantity(unit: HKUnit.meter(), doubleValue: distance)
    
    // This sample is very similar to the heart rate sample. However as this time the data is over two points, we pass separate start and end dates. 
    let sample = HKQuantitySample(type: HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.distanceWalkingRunning)!, quantity: quantity, start: previousTimeStamp, end: trackpoint.timeStamp)
    
    // Add the sample to array which setup before, which will already contain our heart rate samples.

    samples.append(sample)
}

// Finally set the previous values.                         
previousLocation = location
previousTimeStamp = trackpoint.timeStamp                          

I know that’s a lot of code, but now we have all our samples, location data and total distance ready to link to our workout. In case you missed it, the totalDistance variable we set up above was the same one we used to initialise the HKWorkout object before.

The final phase is to add the samples to the workout and then finish the route builder. Both of these steps can only be done once a workout has been stored within HealthKit.

This is perfect as we already stored our workout in HealthKit earlier on.

// If you don't remember, this was the code be wrote earlier to save our workout to HealthKit. Now we are going to add our samples and complete the route builder.

// Instead of repeatedly creating health stores I have just stored one in a constant to improve readability. 
let store = HKHealthStore()

store.save(hkworkout) { (finished, error) in
  if finished {
      
      // We first add the samples to our hkworkout instance.
      store.add(samples, to: hkworkout, completion: { (finished, error) in })

      // To store all the location information we just have to finish the routeBuilder and pass in our hkworkout.
      routeBuilder.finishRoute(with: hkworkout, metadata: nil, completion: { (route, error) in })        

  }
}

Once the samples have been added, and finishRoute: has been called on the route builder, your workout in HealthKit will now be populated with all that extra information.

I hope that gives you a good insight into how to add data into HealthKit.

Related posts

Uploading content, React, GraphQL, Rails and Active Storage

Rowan
5 years ago

Running Xcode Tests from CI

Rowan
5 years ago

Importing File Data

Rowan
6 years ago
Exit mobile version