This is a series of tutorials that will walkthrough:
- Obtaining Consent and Generating PDF
- Creating a Heart Rate Task + Heart Rate with Apple Watch
- Displaying the Results as a chart + Send via HTTP
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
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
, theHeartRateTask
will also be anORKOrderedTask
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
:
@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:
Yay!
But wait...where do we get the heart rate data? That is why there is a section 2
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...:
And select WatchKit App from watchOS:
Yes, we will activate scheme:
Go to project settings, select watch extension as target:
And enable HealthKit for the watch extension:
Open Interface.storyboard
in your Watch App folder, add a label and a button to Interface Controller Scene
:
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:
I'll just call them
button
andlabel
, 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
:
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:
The heart rate reading will also be avaliable in the Health App:
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
- Accessing Heart Rate Data for Your ResearchKit Study
- watchOS-3-heartrate
- ResearchKit sample projects
Note to myself: Renaming an Xcode Project is a pain