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
The ResearchKit framework provides classes that allow you to display data in charts and graphs. Presenting information this way can help users understand your data better and provide key insights in a visual way.
In part 2, we have successfully written heart rate data into Health. In this part, we will query that data and visualize it with ResearchKit.
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
Unlike the continous query we used last time for displaying heart rate on the Watch, this time, we will be querying for heart rate samples in the time interval of our active task. More specifically, we will be HKSampleQuery.
The sample query returns sample objects that match the provided type and predicate. You can provide a sort order for the returned samples, or limit the number of samples returned.
--- Swift Documentation
Thus, create ResultParser.swift under Tasks
group and add the following code:
import Foundation
import HealthKit
import ResearchKit
struct ResultParser{
static func getHKData(startDate: Date, endDate: Date){
let healthStore = HKHealthStore()
let hrType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
let sortDescriptors = [NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: true)]
let hrQuery = HKSampleQuery(sampleType: hrType, predicate: predicate, limit: Int(HKObjectQueryNoLimit), sortDescriptors: sortDescriptors){
(query:HKSampleQuery, results:[HKSample]?, error: Error?) -> Void in
DispatchQueue.main.async {
guard error == nil else {
print("Error: \(String(describing: error))")
return
}
guard let results = results as? [HKQuantitySample] else {
print("Data conversion error")
return
}
if results.count == 0 {
print("Empty Results")
return
}
for result in results{
print("HR: \(ORKValueRange(value: result.quantity.doubleValue(for: HKUnit(from: "count/min"))))")
}
}
}
healthStore.execute(hrQuery)
}
}
As you can see from the code, we would need to pass in a start date and an end date for the query. Luckily, ResearchKit allows us to access the starting and ending dates of our heart rate task easily.
Add the following to ResultParser.swft
for access across files:
struct TaskResults{
static var startDate = Date.distantPast
static var endDate = Date.distantFuture
}
Note that here
distantPast
anddistantFuture
is used to tell if user has completed the task or not
Open TasksViewController.swift
and modify
taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?)
to save the starting and ending dates of the timed step in our heart rate task:
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
taskViewController.dismiss(animated: true, completion: nil)
if reason == .completed && taskViewController.result.identifier == "HeartRateTask"{
if let results = taskViewController.result.results, results.count > 2 {
if let hrResult = results[2] as? ORKStepResult{
TaskResults.startDate = hrResult.startDate
TaskResults.endDate = hrResult.endDate
print("Start Date: \(TaskResults.startDate)\nEnd Date: \(TaskResults.endDate)\n")
}
}
}
}
Build, run and go through the heart rate task. Once completed, the dates will be printed to console:
Having coded the necessary functions to access our heart rate data from Health, we can add a view controller where we will have a chart to display the results and a button for refreshing the data (as heart rate data is not pushed to Health in real-time)
Create ResultsViewController under Tasks
group, add some method stub:
import UIKit
import ResearchKit
class ResultsViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//A refresh func
}
Then add a new view controller "Results View Controller" in Main.storyboard
and add a Results button to Tasks View Controller
which triggers a Show (e.g. Push)
segue to it:
Add a Refresh and a Back button to Results View Controller
. While holding control
key, click and drag Back
to the red exit icon on top of Results View Controller
to link to unwindToTasks:
unwind segue:
Set its Custom Class
to ResultsViewController
:
And add an action outlet for Refresh
button:
On tapping Refresh
, we will call refresh() which will call getHKData(startDate: Date, endDate: Date)
to get the heart rate data. Of course we will only call it when we have completed the task, or when the dates are not distantPast
and distantFuture
:
@IBAction func refreshButtonTapped(_ sender: UIButton) {
refresh()
}
func refresh(){
if(TaskResults.startDate != Date.distantPast && TaskResults.endDate != Date.distantFuture){
ResultParser.getHKData(startDate: TaskResults.startDate, endDate: TaskResults.endDate)
}
}
Build and run the app. After collecting heart rate data during heart rate task with Apple Watch, tap Results
to open up Results View Controller
and tap Refresh
.
After some repeated furious tapping
Heart rate data will be retrived and printed to console:
ResearchKit includes a Charts module. It features three chart types: a pie chart (
ORKPieChartView
), a line graph chart (ORKLineGraphChartView
), and a discrete graph chart (ORKDiscreteGraphChartView
).
For our heart rate data, we will use an ORKLineGraphChartView
to show the change of heart rate during our active task.
So, add a View to Results View Controller
and set its Custom Class
to ORKLineGraphChartView
and create an outlet in ResultsViewController
, call it graphChartView:
An ORKGraphChartView
plots the graph according to its dataSource
, which is an ORKGraphChartViewDataSource
object. Thus, we will create HeartRateDataSource.swift under Tasks
group that extends ORKGraphChartViewDataSource
:
import ResearchKit
class HeartRateDataSource: NSObject, ORKValueRangeGraphChartViewDataSource{
var plotPoints = [ORKValueRange]()
func numberOfPlots(in graphChartView: ORKGraphChartView) -> Int {
return 1
}
func graphChartView(_ graphChartView: ORKGraphChartView, dataPointForPointIndex pointIndex: Int, plotIndex: Int) -> ORKValueRange {
return plotPoints[pointIndex]
}
func graphChartView(_ graphChartView: ORKGraphChartView, numberOfDataPointsForPlotIndex plotIndex: Int) -> Int {
return plotPoints.count
}
//A func to update the data source
}
As heart rate data is not pushed to Health in real-time, the data may not be avaliable to us when we show Results View Controller
, thus we would need to update the data source and re-plot the chart.
However, since ORKGraphChartView
cannot use a mutable ORKGraphChartViewDataSource
(var
), we will have to write a function that updates plotPoints
of our instance of HeartRateDataSource
.
(Note to myself: That is a painful realization after a lot of failed attempts)
Replace //A func to update the data source
with:
func updatePlotPoints(newPlotPoints: [ORKValueRange]){
self.plotPoints = newPlotPoints
}
Now we are ready to save our data to a data source. Add static var hrPlotPoints = [ORKValueRange]()
to struct TaskResults
and modify getHKData(startDate: Date, endDate: Date)
to save the heart rate data:
static func getHKData(startDate: Date, endDate: Date){
let healthStore = HKHealthStore()
let hrType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
let sortDescriptors = [NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: true)]
let hrQuery = HKSampleQuery(sampleType: hrType, predicate: predicate, limit: Int(HKObjectQueryNoLimit), sortDescriptors: sortDescriptors){
(query:HKSampleQuery, results:[HKSample]?, error: Error?) -> Void in
DispatchQueue.main.async {
guard error == nil else {
print("Error: \(String(describing: error))")
return
}
guard let results = results as? [HKQuantitySample] else {
print("Data conversion error")
return
}
if results.count == 0 {
print("Empty Results")
return
}
for result in results{
print("HR: \(ORKValueRange(value: result.quantity.doubleValue(for: HKUnit(from: "count/min"))))")
TaskResults.hrPlotPoints.append(ORKValueRange(value: result.quantity.doubleValue(for: HKUnit(from: "count/min"))))
}
}
}
healthStore.execute(hrQuery)
}
Declare let dataSource = HeartRateDataSource()
in ResultsViewController
and modify viewDidLoad()
to setup of the graph after loading the view:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
graphChartView.dataSource = dataSource
dataSource.updatePlotPoints(newPlotPoints: TaskResults.hrPlotPoints)
graphChartView.tintColor = UIColor(red: 255/255, green: 41/255, blue: 135/255, alpha: 1)
}
Modify refresh()
to update data source and re-plot the chart when Refresh
button is pressed:
func refresh(){
if(TaskResults.startDate != Date.distantPast && TaskResults.endDate != Date.distantFuture){
ResultParser.getHKData(startDate: TaskResults.startDate, endDate: TaskResults.endDate)
dataSource.updatePlotPoints(newPlotPoints: TaskResults.hrPlotPoints)
graphChartView.reloadData()
}
}
We will also refresh each time when the view will appear:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
refresh()
}
Build and run the app. Collect some heart rate data and open results. After refreshing a few times, the graph will be plotted:
Suppose you plan to distribute your research app to collect data, it wouldn't be helpful if the data remained locally on the iOS devices. Thus, this section will go through how to send the data via HTTP as JSON.
Before sending as JSON, we need to convert our results into a valid JSON object. In Swift, a [String: String]
dictionary is such an object. For our purposes, we will be sending the heart rate, the starting date and the ending date.
Add the following method to ResultParser
to convert a HKQuantitySample
to [String:String]
:
static func sampleToDict(sample: HKQuantitySample) -> [String: String]{
var dict: [String:String] = [:]
dict["hr"] = "\(sample.quantity.doubleValue(for: HKUnit(from: "count/min")))"
dict["startDate"] = "\(sample.startDate)"
dict["endDate"] = "\(sample.endDate)"
return dict
}
And a function that will serialize an Object to JSON and send via a HTTP Post request:
static func resultViaHTTP(results: [HKQuantitySample]){
var toSend: [[String: String]] = []
for result in results{
toSend.append(sampleToDict(sample: result))
}
var request = URLRequest(url: URL(string: "<Your Receiver URL Here>")!)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
if JSONSerialization.isValidJSONObject(toSend){
do{
let data = try JSONSerialization.data(withJSONObject: toSend, options: JSONSerialization.WritingOptions.prettyPrinted)
request.httpBody = data
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if let error = error{
print(error)
return
}
}
task.resume()
}catch{
print("Error while sending JSON via HTTP")
}
}else{
print("Invalid JSON Object")
}
}
Finally, we will call the method in getHKDate(startDate: Date, endDate: Date)
after we print heart rate to console:
for result in results{
print("HR: \(ORKValueRange(value: result.quantity.doubleValue(for: HKUnit(from: "count/min"))))")
TaskResults.hrPlotPoints.append(ORKValueRange(value: result.quantity.doubleValue(for: HKUnit(from: "count/min"))))
}
resultViaHTTP(results: results)
Build and run the app. Collect some heart rate data and open results. After refreshing a few times, the data will be sent as JSON while the graph is plotted:
Yay!
The completed tutorial project is avaliable here
This Marks the end of the series. Thank you for reading!