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
As Apple described, Research Kit is a handy tool to create medical research apps, which would most probably involve collecting user data. Before collecting any data, as a researcher, you would need to obtain consent from the user by explaining to them what will happen to the data you collected, and kindly ask the users to sign the consent.
This part of the tutorial will walk through how to "easily create visual consent flows", ask for Health data authorization, create a passcode to lock the app and generate a pdf version of the consent document.
Note that this tutorial assumes that you have some prior experiences in Swift
Note that this tutorial is written in Swift 3
So, let's begin
Go ahead and open up your Xcode, create a new Xcode project, and select Single View App from iOS. Give your project a nice name and store it at a place you promise you won't forget.
After creating a new project, delete ViewController.swift
and open up Main.storyboard
.
Now, for the research, we would only need to obtain the users' consent once as they join the study. If they have consented, we would like to show them the tasks screen directly.
So, in Main.storyboard
, add 2 more view controllers to the screen.
The layout of the storyboard is a matter of religion, not programming
And on one of the view controllers you just added, add a Join Study
button.
Also, you may want to add some constaints so Xcode doesn't complain
From this point on, we will call the view controller that you just added a button to Consent View Controller and the blank one Tasks View Controller, as it will be used for buttons for tasks in the later parts of the series.
Good. Now, we need some way for the user to reach the button from the Initial View Controller
(the one with an arrow pointing into it from the void). To do that, we will create a custom segue from to Consent View Controller
.
Segue \ˈsɛɡweɪ\
A segue refers to a transition from one scene to another in a storyboard
--- App Development with Swift
While holding the control
key, click and drag the yellow icon from the Initial View Controller
to Consent View Controller
. You will see a small window like the one shown below.
Select Custom
. You will now see an arrow pointing from Initial View Controller
to Consent View Controller
like below.
Repeat the same for Tasks View Controller
. The reason to choose custom segues here is that we want the next view controller to be presented directly without any animation. We will program those soon, but before we do so, those segues need to be named for us to refer to them in our code.
Select the segue by clicking on the arrow, and give them Identifiers
like in the screenshots below.
I call them
toConsent
andtoTasks
respectively because, well, they lead toConsent View Controller
andTasks View Controller
respectively
Create a new group called Intro, and in the group, create IntroViewController.swift and IntroSegue.swift and those will contain code that decides what to show to the user and how to do so.
In IntroSegue.swift
, add the following code:
import UIKit
class IntroSegue: UIStoryboardSegue{
override func perform() {
let controllerToReplace = source.childViewControllers.first
let destinationControllerView = destination.view
destinationControllerView?.translatesAutoresizingMaskIntoConstraints = true
destinationControllerView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
destinationControllerView?.frame = source.view.bounds
controllerToReplace?.willMove(toParentViewController: nil)
source.addChildViewController(destination)
source.view.addSubview(destinationControllerView!)
controllerToReplace?.view.removeFromSuperview()
destination.didMove(toParentViewController: source)
controllerToReplace?.removeFromParentViewController()
}
}
This code will instruct our custom segues to replace the old view controller with the new one, without any animation (did I mention that before?)
Now, go back to Main.storyboard
and set the Class
of the two custom segues to IntroSegue
, shown below:
Then, in IntroViewController.swift
, add the following code:
import UIKit
class IntroViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
toConsent()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func toConsent(){
performSegue(withIdentifier: "toConsent", sender: self)
}
func toTasks(){
performSegue(withIdentifier: "toTasks", sender: self)
}
}
At this stage, we will directly head off to
ConsentViewController
(as seen inviewDidLoad()
)Important: in
performSegue(withIdentifier: ,sender:)
use the identifier you gave to the segues)
And to apply the code to Intro View Controller
, set its Class
to IntroViewController.swift
, as shown below:
Build and run your app (Either in simulator or on an actual phone), you will be shown Join Study
button on app launch:
Yay!
"But", you say, "The button doesn't work." Well, we will take care of that in section 2
Hmm...seems like I forgot something...
This is a ResearchKit tutorial...ah, right! We need to import ResearchKit
Since ResearchKit is an open source project, you would have to clone it from github first. To do that, you can either go directly here, Download ZIP and unzip it.
Or you can open Terminal and type:
git clone https://github.com/ResearchKit/ResearchKit.git
What do you mean git command not found? Oh you don't have git? Then you can install Homebrew and use it to install git:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install git
Then, go into the ResearchKit folder and drag and drop the ResearchKit.xcodeproj
into your project:
Yup, I named my project ResearchKitConsent
Then, open your project setting (by clicking on the xcodeproject icon with your project name), scroll to the bottom of General
, and in embedded binaries section, click +
, and add ResearchKit.frameworkiOS
:
After adding, the embedded binaries section should look like this:
Good. Now, create a new group called Consent, create ConsentViewController.swift, and add the following code:
import ResearchKit
class ConsentViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
To make the button actually work, we will set the Class
of Consent View Controller
to ConsentViewController
. (It is a mouthful to read, but matching names makes everyone's life easier).
Then, open Assistant Editor "" (It should show ConsentViewController.swift
. If not, select the file manually), and while holding control
key, click the button and drag into your code. Select connection type as Action
and give it an appropriate name:
I call it joinButtonTapped because, well, it does something when I tap
Join Study
But before we add anything to the function stub, we will create our consent document. Create a file called ConsentDocument.swift under group Consent
. Add the following code:
import ResearchKit
public var ConsentDocument: ORKConsentDocument{
let consentDocument = ORKConsentDocument()
consentDocument.title = "Research Study Consent Form" //Or other title you prefer
let sectionTypes: [ORKConsentSectionType] = [
.overview,
.dataGathering,
.privacy,
.dataUse,
.timeCommitment,
.studySurvey,
.studyTasks,
.withdrawing
]
//Section contents
//Add sections
//Signature
return consentDocument
}
This part of the code tells your app what sections your consent document will contain
Then replace //Section contents
with a String[]
of the same size, with each element of the list initialized to a string containing corresponding text for each section. For example:
let text = [
"Section 1: Welcome. This study is about...",
"Section 2: Data Gathering. This study will collect data from your Apple Watch...",
"Section 3: Privacy. We value your privacy...",
"Section 4: Data Use. The data collected will be used for...",
"Section 5: Time Commitment. This study will take you roughly...",
"Section 6: Study Survey. For this study, you will need to fill out a survey...",
"Section 7: Study Tasks. You will be requested to do these tasks...",
"Section 8: Withdrawing. To withdraw from the study..."
]
You can always use
lorem ipsum
as placeholder
To add the sections to the document, replace //Add sections
with the code below:
for sectionType in sectionTypes {
let section = ORKConsentSection(type: sectionType)
let localizedText = NSLocalizedString(text[sectionTypes.index(of: sectionType)!], comment: "")
let localizedSummary = localizedText.components(separatedBy: ".")[0] + "."
section.summary = localizedSummary
section.content = localizedText
if consentDocument.sections == nil {
consentDocument.sections = [section]
} else {
consentDocument.sections!.append(section)
}
}
Good old for loop
In order to render the consent document into a pdf, ResearchKit
requires the signature field to have a title. (Otherwise you will get a SIGABRT when attempting to generate a PDF. Not sure why this is not included in most of the ResearchKit tutorials). So, replace //Signature
with:
consentDocument.addSignature(ORKConsentSignature(forPersonWithTitle: "Participant", dateFormatString: nil, identifier: "ConsentDocumentParticipantSignature"))
We will call our participant "Participant". Perfect!
Next, we will use our ConsentDocument
to create a Consent Task. Create ConsentTask.swift under Consent
group.
Our ConsentTask
will be an ORKOrderedTask
:
The ORKOrderedTask class implements all the methods in the ORKTask protocol and represents a task that assumes a fixed order for its steps.
In the ResearchKit framework, any simple sequential task, such as a survey or an active task, can be represented as an ordered task.
--- ResearchKit Documentation
And thus our task will consist of a list of sequential tasks:
import ResearchKit
public var ConsentTask: ORKOrderedTask{
var steps = [ORKStep]()
//Visualization
//This step is where Apple will help us present the consent document with animations
//Request Health Data
//We will need the user to grant us access to health data for collection
//Review & Sign
//The whole document will be shown to the user and the user will draw their signature on the touch screen
//Passcode/TouchID Protection
//Because it is personal data
//Completion
//A thank you message
return ORKOrderedTask(identifier: "ConsentTask", steps: steps)
}
As Apple mentioned, ResearchKit
provides easy visualization of the consent document. So, we will replace
//This step is where Apple will help us present the consent document with animations
with
let consentDocument = ConsentDocument
let visualConsentStep = ORKVisualConsentStep(identifier: "VisualConsentStep", document: consentDocument)
steps += [visualConsentStep]
replace
//The whole document will be shown to the user and the user will draw their signature on the touch screen
with
let signature = consentDocument.signatures!.first!
let reviewConsentStep = ORKConsentReviewStep(identifier: "ConsentReviewStep", signature: signature, in: consentDocument)
reviewConsentStep.text = "Review the consent form."
reviewConsentStep.reasonForConsent = "Consent to join study"
steps += [reviewConsentStep]
and replace
//A thank you message
with
let completionStep = ORKCompletionStep(identifier: "CompletionStep")
completionStep.title = "Welcome aboard."
completionStep.text = "Thank you for joining this study."
steps += [completionStep]
At this stage, we have a functional ConsentTask
and let's try it out! Remember joinButtonTapped(_ sender: UIButton)
from ConsentViewController
? Add the code below:
@IBAction func joinButtonTapped(_ sender: UIButton) {
let taskViewController = ORKTaskViewController(task: ConsentTask, taskRun: nil)
taskViewController.delegate = self
present(taskViewController, animated: true, completion: nil)
}
Let's build and....
...wait, there is an error at taskViewController.delegate = self
Ah, right, our ConsentViewController
is not an ORKTaskViewControllerDelegate yet. Our ConsentViewController
will need to extend ORKTaskViewControllerDelegate
in order to tell taskViewController
what to do when we finish our ConsentTask
.
So, add the following code to ConsentViewController.swift
:
extension ConsentViewController: ORKTaskViewControllerDelegate{
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
dismiss(animated: true, completion: nil) //Dismisses the view controller when we finish our consent task
}
}
Now, build, run, and click the Join Study
button:
Yay! Also, nice animation! Thanks Apple!
Remember in ConsentTask.swift
we haven't replace //We will need the user to grant us access to health data for collection
right?
For this step in the task, we will first enable HealthKit for our app. Go to Capabilities
of project settings and enable HealthKit
:
And we will add some description strings to Info.plist
:
Apple will not let us access health data unless we explain to users explicitly what we will do with it
Then, we will have to create our own ORKActiveStep
in order to show the Health authorization screen.
Note that usually ORKActiveStep will be used to collect data in real time. Here we will use it because ORKInstructionStep
uses a Get Started
button, which may confuse the user
Create HealthDataAuthStep.swift
under Consent
group and add the following code (suppose we are collecting heart rate data):
import HealthKit
import ResearchKit
class HealthDataAuthStep: ORKActiveStep {
let healthKitStore = HKHealthStore()
let healthDataItemsToRead: Set<HKObjectType> = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!]
let healthDataItemsToWrite: Set<HKSampleType> = []
override init(identifier: String) {
super.init(identifier: identifier)
title = NSLocalizedString("Health Data", comment: "")
text = NSLocalizedString("On the next screen, you will be prompted to grant access to read your heart rate data for this study.\n\nIf you have declined authorization before and wish to grant access, head to\n\n Settings -> Privacy -> Health\n\nto authorize.", comment: "")
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func getHealthAuthorization(_ completion: @escaping (_ success: Bool, _ error: NSError?) -> Void) {
HKHealthStore().requestAuthorization(toShare: healthDataItemsToWrite, read: healthDataItemsToRead){ (success, error) -> Void in
completion(success, error as NSError?)
}
}
}
There are also other data types avaliable from HealthKit
And because we just coded a custom ORKActiveTask
, we would also need an ORKActiveStepViewController
to go with it. Create HealthDataAuthStepViewController.swift
under Consent
group and add the following code:
import HealthKit
import ResearchKit
class HealthDataAuthStepViewController: ORKActiveStepViewController {
var healthDataStep: HealthDataAuthStep? {
return step as? HealthDataAuthStep
}
override func goForward() {
healthDataStep?.getHealthAuthorization(){succeeded, _ in
guard succeeded || (TARGET_OS_SIMULATOR != 0) else { return }
OperationQueue.main.addOperation {
super.goForward()
}
}
}
}
In ConsentViewController.swift
add the following method to the extension so our app will use the view controller we just created:
func taskViewController(_ taskViewController: ORKTaskViewController, viewControllerFor step: ORKStep) -> ORKStepViewController? {
if step is HealthDataAuthStep {
let healthStepViewController = HealthDataAuthStepViewController(step: step)
return healthStepViewController
}
return nil
}
And we will finally replace //We will need the user to grant us access to health data for collection
with:
let healthDataStep = HealthDataAuthStep(identifier: "Health")
steps += [healthDataStep]
Now you have a glipse of what need to happen so we can use "easy consent flow"
Build and run the app. The consent task will now contain a step that asks for Health data authorization.
Note that this window only appears once. If the user declines access to health data here, they will need to go to Settings -> Privacy
to grant access
The other step that we have not replaced //Because it is personal data
with proper passcode protection step. So, here we go:
let passcodeStep = ORKPasscodeStep(identifier: "Passcode")
passcodeStep.text = "Now you will create a passcode to identify yourself to the app and protect access to information you've entered."
steps += [passcodeStep]
You probably realized if this is included as a seperate section in this tutorial, it won't be that easy. You are right.
For the passcode step to work, we would first need to tell our app what to do. Open up AppDelegate.swift
and add the following extension to make it a ORKPasscodeDelegate
:
extension AppDelegate: ORKPasscodeDelegate {
func passcodeViewControllerDidFinish(withSuccess viewController: UIViewController) {
containerViewController?.contentHidden = false
viewController.dismiss(animated: true, completion: nil)
}
func passcodeViewControllerDidFailAuthentication(_ viewController: UIViewController) {
}
}
We would also need to lock the app when launching or returning to foreground if there is a passcode stored. So, add the following method to AppDelegate
:
func lockApp() {
/*
* Only lock the app if there is a stored passcode and a passcode
* controller isn't already being shown.
*/
guard ORKPasscodeViewController.isPasscodeStoredInKeychain() && !(containerViewController?.presentedViewController is ORKPasscodeViewController) else { return }
window?.makeKeyAndVisible()
let passcodeViewController = ORKPasscodeViewController.passcodeAuthenticationViewController(withText: "Welcome back ", delegate: self)
containerViewController?.present(passcodeViewController, animated: false, completion: nil)
}
Since we will be hiding the content, go to IntroViewController
and add:
var contentHidden = false {
didSet {
guard contentHidden != oldValue && isViewLoaded else { return }
childViewControllers.first?.view.isHidden = contentHidden
}
}
And the app will need to know when to lock the app. import ResearchKit
and add the following to AppDelegate.swift
:
var containerViewController: IntroViewController? {
return window?.rootViewController as? IntroViewController
}
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let standardDefaults = UserDefaults.standard
if standardDefaults.object(forKey: "TutorialFirstRun") == nil {
ORKPasscodeViewController.removePasscodeFromKeychain()
standardDefaults.setValue("TutorialFirstRun", forKey: "TutorialFirstRun")
}
// Appearance customization
let pageControlAppearance = UIPageControl.appearance()
pageControlAppearance.pageIndicatorTintColor = UIColor.lightGray
pageControlAppearance.currentPageIndicatorTintColor = UIColor.black
return true
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
lockApp()
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
if ORKPasscodeViewController.isPasscodeStoredInKeychain() {
// Hide content so it doesn't appear in the app switcher.
containerViewController?.contentHidden = true
}
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
lockApp()
}
Build and run the app. You will see a passcode step after you draw your signature:
When the app is launched or brought to foreground the next time, it will ask for the passcode:
The fact that there is a passcode store also presents us a way of knowing if the user has consented or not.
Open IntroViewController
and change viewDidLoad()
to lead the user directly to Tasks View Controller
on launch if they have consented:
import ResearchKit
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if ORKPasscodeViewController.isPasscodeStoredInKeychain(){
toTasks()
}else{
toConsent()
}
}
We would also need to lead user to Tasks View Controller
when they complete the consent task.
Add the following code to IntroViewController
:
@IBAction func unwindToTasks(_ segue: UIStoryboardSegue){
toTasks()
}
Then, open up Main.storyboard
, select Consent View Controller
and, while holding control
key, click and drag the yellow view controller icon to the exit icon. Select unwindToTasks:
:
Give the unwind segue identifier unwindToTasks
:
And in ConsentViewController: ORKTaskViewControllerDelegate
, modify
taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?)
so the unwind segue is performed when consent task is complete:
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
switch reason{
case .completed:
performSegue(withIdentifier: "unwindToTasks", sender: nil)
case .discarded, .failed, .saved:
dismiss(animated: true, completion: nil)
}
}
The passcode store also presents us an easy way for the user to leave our study. Create a new group called Tasks and in the group, create TasksViewController.swift. Initialize the file with the standard UIViewController
stub:
import UIKit
import ResearchKit
class TasksViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Assign TasksViewController
as Tasks View Controller
's custom class in Main.storyboard
, and add a button Leave Study:
Then, while holding control
key, click and drag Leave Study
button to Consent View Controller
and select Show
segue:
We shall call it returnToConsent:
Next, give the button some action. Open Assistant Editor "", and add an Action
connection to TasksViewController
. We shall name it leaveButtonTapped:
In leaveButtonTapped(_ sender: UIButton)
, add the following code which deletes the passcode store and present Consent View Controller
:
ORKPasscodeViewController.removePasscodeFromKeychain()
performSegue(withIdentifier: "returnToConsent", sender: nil)
This way, the user will leave the study, and if they wish to join again, they will be presented the consent document and requested to create a passcode.
Build, run and try out the cycle. Yay!
Luckily, ResearchKit
not only has the tool to visualize the consent process, but also the tool to save the signed consent document as a PDF.
Modify
taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?)
in ConsentViewController: ORKTaskViewControllerDelegate
to:
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
switch reason{
case .completed:
let signatureResult: ORKConsentSignatureResult = taskViewController.result.stepResult(forStepIdentifier: "ConsentReviewStep")?.firstResult as! ORKConsentSignatureResult
let consentDocument = ConsentDocument.copy() as! ORKConsentDocument
signatureResult.apply(to: consentDocument)
consentDocument.makePDF{ (data, error) -> Void in
var docURL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).last
docURL = docURL?.appendingPathComponent("consent.pdf")
try? data?.write(to: docURL!, options: .atomicWrite)
}
performSegue(withIdentifier: "unwindToTasks", sender: nil)
case .discarded, .failed, .saved:
dismiss(animated: true, completion: nil)
}
}
TL:DR: Apply the signature to the document and save as a PDF
Now, add a button Consent Document to Tasks View Controller
and create an action connection in Tasks View Controller
:
Then add the following code to the button handler:
let taskViewController = ORKTaskViewController(task: consentPDFViewerTask(), taskRun: nil)
taskViewController.delegate = self
present(taskViewController, animated: true, completion: nil)
At this point, Xcode may throw you some errors. This is because we haven't create the PDF task nor its delegate yet.
Add the following code to TasksViewController
to create a PDF task that will open our consent document:
func consentPDFViewerTask() -> ORKOrderedTask{
var docURL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).last
docURL = docURL?.appendingPathComponent("consent.pdf")
let PDFViewerStep = ORKPDFViewerStep.init(identifier: "ConsentPDFViewer", pdfURL: docURL)
PDFViewerStep.title = "Consent"
return ORKOrderedTask(identifier: String("ConsentPDF"), steps: [PDFViewerStep])
}
and the following extension to tell the app what to do when the user closes the PDF viewer:
extension TasksViewController: ORKTaskViewControllerDelegate{
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
taskViewController.dismiss(animated: true, completion: nil)
}
}
Build and run. Complete the consent, and tap Consent Document
:
Voilà!
And we are done with consent. Yay!
Until I see you in Part 2, farewell!
The completed tutorial project is avaliable here