Skip to content

Instantly share code, notes, and snippets.

@sye8
Last active December 11, 2020 14:27
Show Gist options
  • Save sye8/369858d4e769b5bcd44f3aaa5fced9c4 to your computer and use it in GitHub Desktop.
Save sye8/369858d4e769b5bcd44f3aaa5fced9c4 to your computer and use it in GitHub Desktop.
Research Kit + Apple Watch Tutorial (Swift 3) - Part 3: Displaying the Results as a chart + Send via HTTP

Research Kit + Apple Watch Tutorial

This is a series of tutorials that will walkthrough:

Part 3: 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.

--- ResearchKit Documentation

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

1. Give me the data!

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 and distantFuture 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:

consoledates

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:

toresults

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:

unwindtotasks

Set its Custom Class to ResultsViewController:

resultscustomclass

And add an action outlet for Refresh button:

refreshoutlet

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.

buttontapping

After some repeated furious tapping

Heart rate data will be retrived and printed to console:

hrprint

2. I would rather read a chart

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).

--- ResearchKit GitHub Repo README

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:

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:

3. Not very useful locally

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:

json

Yay!


The completed tutorial project is avaliable here

This Marks the end of the series. Thank you for reading!

Referenced Projects

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