Skip to content

Instantly share code, notes, and snippets.

@sye8
Last active January 24, 2024 14:19
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sye8/d67d59f952d720113eda68d4bf04f371 to your computer and use it in GitHub Desktop.
Save sye8/d67d59f952d720113eda68d4bf04f371 to your computer and use it in GitHub Desktop.
Research Kit + Apple Watch Tutorial (Swift 3) - Part 2: Creating a Heart Rate Task + Heart Rate with Apple Watch

Research Kit + Apple Watch Tutorial

This is a series of tutorials that will walkthrough:

Part 2: Creating a Heart Rate Task + Heart Rate with Apple Watch

ResearchKit is an open source framework introduced by Apple that allows researchers and developers to create powerful apps for medical research. Easily create visual consent flows, real-time dynamic active tasks, and surveys using a variety of customizable modules that you can build upon and share with the community. And since ResearchKit works seamlessly with HealthKit, researchers can access even more relevant data for their studies — like daily step counts, calorie use, and heart rate.

--- ResearchKit.org

Part 1 of the series went throught how to create visual consent flows. In this part, we will continue from where we left off and create a real-time dynamic active task.

Suppose as a researcher, you want to collect heart rate data during some activity. Lucky for you Apple Watch provides near real-time recording of heart rate and can be incorporated into your research app.

Note that this tutorial assumes that you have some prior experiences in Swift

Note that this tutorial is written in Swift 3

Note that this part will not work on the Simulator

1. Get moving!

Create HeartRateTask.swift under group Tasks. Add the following code:

import ResearchKit

public var HeartRateTask: ORKOrderedTask {

    var steps = [ORKStep]()
    
    //Instruction Step
    //Tell the user what to do
    
    //Start Prompt Step
    //Press the 'Start Recording' button on Watch extension
    
    //Heart Rate Step
    //Record heart rate during some activity
    
    //End Prompt Step
    //Press the 'End Recording' button on Watch extension
    
    //Summary Step
    //Thank you!
    
    return ORKOrderedTask(identifier: "HeartRateTask", steps: steps)
}

Just like our ConsentTask, the HeartRateTask will also be an ORKOrderedTask consisting of several steps in a sequence.

In the heart rate task, we will ask the user to start recording heart rate from their Apple Watch, instruct the user to do some activity, and then stop recording from their watch.

(Sadly we cannot force Apple Watch to do anything from iPhone. We have to prompt the user.)

So, we will replace //Tell the user what to do with:

let instructionStep = ORKInstructionStep(identifier: "Instruction")
instructionStep.title = "Heart Rate"
instructionStep.text = "<Description>"
steps += [instructionStep]

and //Press the 'Start Recording' button on Watch extension with:

let startPromptStep = ORKActiveStep(identifier: "StartPrompt")
startPromptStep.title = "Heart Rate"
startPromptStep.text = "Please open the corresponding app on your watch and press \"Start Recording\"."
steps += [startPromptStep]

The next step involves more customization as we want to show the user a timer and automatically moves to the step after this once the time is up. Replace //Record heart rate during some activity with:

let heartrateStep = ORKActiveStep(identifier: "HeartRate")
heartrateStep.stepDuration = 30 //In seconds
heartrateStep.shouldShowDefaultTimer = true
heartrateStep.shouldStartTimerAutomatically = true
heartrateStep.shouldContinueOnFinish = true
heartrateStep.title = "Heart Rate"
heartrateStep.text = "Please <do sth> for 30 seconds."
steps += [heartrateStep]

Note that in this step we are not using the recorder function from ResearchKit as health data is not pushed from Watch to iPhone in realtime. We will query Health for heart rate data in Part 3.

The rest are pretty much symmetric to the first 2 steps:

//End Prompt Step
let endPromptStep = ORKActiveStep(identifier: "EndPrompt")
endPromptStep.title = "Heart Rate"
endPromptStep.text = "Now you can press \"Stop Recording\" on your watch."
steps += [endPromptStep]
    
//Summary Step
let summaryStep = ORKCompletionStep(identifier: "SummaryStep")
summaryStep.title = "Thank you!"
summaryStep.text = "<text>\nYou can check the results by tapping the results button.\n"
steps += [summaryStep]

Having the task coded, we can now add a button to display the task. Add Heart Rate Task button to Tasks View Controller and create an action connection in TasksViewController:

taskbutton

@IBAction func heartRateButtonTapped(_ sender: UIButton){
    let taskViewController = ORKTaskViewController(task: HeartRateTask, taskRun: nil)
    taskViewController.delegate = self
    present(taskViewController, animated: true, completion: nil)
}

Build, run and tap the button:

hrtaskvc

Yay!

But wait...where do we get the heart rate data? That is why there is a section 2

2. Watching Heart Rate

For this app, we will be collecting heart rate data from Apple Watch. And for that, we need to create an Apple Watch Extension for our app.

Go to File -> New -> Target...:

newtarget

And select WatchKit App from watchOS:

watchkitapp

Yes, we will activate scheme:

watchkitscheme

Go to project settings, select watch extension as target:

selecttarget

And enable HealthKit for the watch extension:

watchhealthkit

Open Interface.storyboard in your Watch App folder, add a label and a button to Interface Controller Scene:

watchlabel

watchbutton

Since we will be modifying the text of the label and the button, we will create outlets to refer to them later in our code in InterfaceController.swift under the watch extension folder:

labeloutlet

buttonoutlet

I'll just call them button and label, because they are the only ones

To measure heart rate on Apple Watch, we would need to create a custom HKWorkoutSession and to display the heart rate, we would need a HKQuery

To do those programmatically, we need to initialize a HKHealthStore:

Use a HKHealthStore object to request permission to share or read HealthKit data. Once permission is granted, you can use the HealthKit store to save new samples to the store, or to manage the samples that your app has saved. Additionally, you can use the HealthKit store to start, stop, and manage queries.

--- Swift Documentation

Thus, import HealthKit and add the following code to InterfaceController.swift:

var isRecording = false
    
//For workout session
let healthStore = HKHealthStore()
var session: HKWorkoutSession?
var currentQuery: HKQuery?

And in willActivate(), we will check if we have Health access permission (which is granted by the user from the iPhone):

override func willActivate() {
    // This method is called when watch view controller is about to be visible to user
    super.willActivate()
    //Check HealthStore
    guard HKHealthStore.isHealthDataAvailable() == true else {
        print("Health Data Not Avaliable")
        return
    }
}

To create and manage a workout session, and to show heart rate during the session, we would need to extend our InterfaceController to be a HKWorkoutSessionDelegate:

The session delegate protocol defines an interface for receiving notifications about errors and changes in the workout session’s state.

--- Swift Documentation

So, add the following code to InterfaceController.swift:

extension InterfaceController: HKWorkoutSessionDelegate{
    func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {
        switch toState {
        case .running:
            //Execute Query
        case .ended:
            //Stop Query
        default:
            print("Unexpected state: \(toState)")
        }
    }
    
    func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
        //Do Nothing
    }
    
    func startWorkout(){
        // If a workout has already been started, do nothing.
        if (session != nil) {
            return
        }
        // Configure the workout session.
        let workoutConfiguration = HKWorkoutConfiguration()
        workoutConfiguration.activityType = .running
        workoutConfiguration.locationType = .outdoor
        
        do {
            session = try HKWorkoutSession(configuration: workoutConfiguration)
            session?.delegate = self
        } catch {
            fatalError("Unable to create workout session")
        }
        
        healthStore.start(self.session!)
        print("Start Workout Session")
    }
    
    //Heart Rate Query
    
}

For displaying heart rate in real-time (actually semi-real-time) we will create a HKAnchoredObjectQuery:

A query that returns only recent changes to the HealthKit store, including a snapshot of new changes and continuous monitoring as a long-running query.

--- Swift Documentation

In our query, when we see a change to Health data (written by Apple Watch during our workout session), we will update the label on the watch interface. Replace //Heart Rate Query with:

func heartRateQuery(_ startDate: Date) -> HKQuery? {
    let quantityType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)
    let datePredicate = HKQuery.predicateForSamples(withStart: startDate, end: nil, options: .strictEndDate)
    let predicate = NSCompoundPredicate(andPredicateWithSubpredicates:[datePredicate])
        
    let heartRateQuery = HKAnchoredObjectQuery(type: quantityType!, predicate: predicate, anchor: nil, limit: Int(HKObjectQueryNoLimit)) { (query, sampleObjects, deletedObjects, newAnchor, error) -> Void in
        //Do nothing
    }
        
    heartRateQuery.updateHandler = {(query, samples, deleteObjects, newAnchor, error) -> Void in
        guard let samples = samples as? [HKQuantitySample] else {return}
        DispatchQueue.main.async {
            guard let sample = samples.first else { return }
            let value = sample.quantity.doubleValue(for: HKUnit(from: "count/min"))
            self.label.setText(String(UInt16(value))) //Update label
        }
    }
        
    return heartRateQuery
}

And we will execute our HKAnchoredObjectQuery when we start our workout session and end the query when the workout session has ended.

Update the code in workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date):

func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {
    switch toState {
    case .running:
        if let query = heartRateQuery(date){
            self.currentQuery = query
            healthStore.execute(query)
        }
    case .ended:
        healthStore.stop(self.currentQuery!)
        session = nil
    default:
        print("Unexpected state: \(toState)")
    }
}

Finally, we will use our button to start and end our workout session.

Create an action connection from to button to InterfaceController.swift:

buttonaction

Since our button will be used to start and stop our workout session, the text on the button must be updated:

@IBAction func buttonTapped() {
    if(!isRecording){
        let stopTitle = NSMutableAttributedString(string: "Stop Recording")
        stopTitle.setAttributes([NSAttributedStringKey.foregroundColor: UIColor.red], range: NSMakeRange(0, stopTitle.length))
        button.setAttributedTitle(stopTitle)
        isRecording = true
        startWorkout() //Start workout session/healthkit streaming
    }else{
        let exitTitle = NSMutableAttributedString(string: "Start Recording")
        exitTitle.setAttributes([NSAttributedStringKey.foregroundColor: UIColor.green], range: NSMakeRange(0, exitTitle.length))
        button.setAttributedTitle(exitTitle)
        isRecording = false
        healthStore.end(session!)
        label.setText("Heart Rate")
    }
}

Build, run and grant Health access from the iOS App. Then install and open your extension and tap Start Recording while wearing the watch. After a few seconds, the Heart Rate label will be updated to your current (and by current, I mean a few seconds ago) heart rate:

watchHeartRate

The heart rate reading will also be avaliable in the Health App:

healthStore

Typically Apple Watch takes a heart rate sample every 5 seconds, so over 30 seconds there will be around 6 samples

But how about data collection? Having the readings in the Health App doesn't really help

That, will be the content of Part 3. Until then, farewell!


The completed tutorial project is avaliable here

Referenced Projects


Note to myself: Renaming an Xcode Project is a pain

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment