ResearchKit in your Swift app

Implementing ResearchKit in your Swift app

mattluedke

In April 2015, Apple introduced ResearchKit, their open-source framework to empower iOS developers to easily make medical research apps. Since then, several new studies (123, among others) have launched with apps based on ResearchKit.

If you're looking for a brand new start in ResearchKit, I wrote a full Getting Started tutorial published on RayWenderlich.com in May. That tutorial will show you how to include ResearchKit in your app, construct consent and survey flows, and even record audio clips.

Screenshot

In this article, I will show you how to upgrade the sample app from the Getting Started tutorial in a couple of key ways, inspired by reader feedback. The main two questions readers sent me about the tutorial were:

  • How do you make survey questions dynamic based on user input and choices?
  • How can you access the sound recording made in the audio task?

By the end of this article, you'll have a ResearchKit app that can do both of these!

Getting Started

First, clone the sample project entitled Camelot from GitHub.

git clone [email protected]:mluedke2/camelot.git

The sample project contains all the work done in this article, so if you'd like to work along, checkout version v1.0, which brings you up to the end of the original tutorial.

cd camelotgit checkout "v1.0"

Open Camelot.xcodeproj, Build & Run, and take a look at the app. When you're familiar with the app and the code underneath, continue for your upgrades!

(Note: this code was written and tested in Xcode 6, as Xcode 7 is still in beta at time of writing).

Make Your Survey Dynamic

Currently, the survey always shows the same questions, in the same order. But with a few upgrades, you can modify survey questions or the question order based on user input.

This example will simply change the wording of the "What is your Quest?" question based on the user's answer to "What is your name?" You can extrapolate from there to make your own changes.

The current SurveyTask is a ORKOrderedTask, because this is the easiest out-of-the-box way to create a simple ORKStep sequence. For this new custom implementation, you'll dive into implementing the ORKTask protocol.

First, create a new Swift file named CustomSurveyTask.swift and match it to the following:

import ResearchKitpublic class CustomSurveyTask: NSObject, ORKTask {  public var identifier: String { get { return "survey"} }  public func stepBeforeStep(step: ORKStep?, withResult result: ORKTaskResult) -> ORKStep? {    // TODO    return nil  }  public func stepAfterStep(step: ORKStep?, withResult result: ORKTaskResult) -> ORKStep? {    // TODO    return nil  }}

You import ResearchKit, then declare a subclass of NSObject that conforms to ORKTask. That protocol requires you to set an identifier for your task, and then implement two methods which will arrange your survey steps in order.

As you'd expect from their names, stepBeforeStep(_:withResult:) andstepAfterStep(_:withResult:) specify which step should come before or after each other step, and together they construct the whole sequence.

These methods are powerful because they aren't simply called once. Instead, they are called repeatedly as the user completes the task. So if you need to change anything about your survey on the fly, you can do it here.

Next, define several String constants as step identifiers at the top of the class:

let introStepID = "intro_step"let nameStepID = "name_step"let questStepID = "quest_step"let colorStepID = "color_step"let summaryStepID = "summary_step"

You'll need these constants in a moment to easily access and define your steps.

Our example of a dynamic survey will add the user's name to the prompt for a later question. So you need a function to construct the question, given a name. Copy this function into your class:

func questStep(name: String?) -> ORKStep {  var questQuestionStepTitle = "What is your quest?"  if let name = name {    questQuestionStepTitle = "What is your quest, \(name)?"  }  let textChoices = [    ORKTextChoice(text: "Create a ResearchKit App", value: 0),    ORKTextChoice(text: "Seek the Holy Grail", value: 1),    ORKTextChoice(text: "Find a shrubbery", value: 2)    ]  let questAnswerFormat: ORKTextChoiceAnswerFormat = ORKAnswerFormat.choiceAnswerFormatWithStyle(.SingleChoice, textChoices: textChoices)  return ORKQuestionStep(identifier: questStepID, title: questQuestionStepTitle, answer: questAnswerFormat)}

Most of the code here is the same as the original step, but the segment at the top includes the user's name in the prompt, if possible.

Next, you construct a method that simply returns the step for a given string identifier. For brevity (and because very little has changed since the original tutorial), I won't paste the entire method here. Instead, simply copy stepWithIdentifier(_:) from the sample code on GitHub into your class.

With each of your steps defined, you're now ready to define their order. First, fill instepBeforeStep(_:withResult:):

public func stepBeforeStep(step: ORKStep?, withResult result: ORKTaskResult) -> ORKStep? {  switch step?.identifier {  case .Some(nameStepID):    return stepWithIdentifier(introStepID)  case .Some(questStepID):    return stepWithIdentifier(nameStepID)  case .Some(colorStepID):    // TODO    return questStep(nil)  case .Some(summaryStepID):    return stepWithIdentifier(colorStepID)  default:    return nil  }} 

This method is simple here, because the order of the steps in this app is constant. A more complicated survey may require more logic.

Now fill in stepAfterStep(_:withResult:):

public func stepAfterStep(step: ORKStep?, withResult result: ORKTaskResult) -> ORKStep? {  switch step?.identifier {  case .None:    return stepWithIdentifier(introStepID)  case .Some(introStepID):    return stepWithIdentifier(nameStepID)  case .Some(nameStepID):    // TODO    return questStep(nil)  case .Some(questStepID):    return stepWithIdentifier(colorStepID)  case .Some(colorStepID):    return stepWithIdentifier(summaryStepID)  default:    return nil  }}

This is very similar, and just specifies the steps in the forward direction. Now that you've filled instepBeforeStep(_:withResult:) and stepAfterStep(_:withResult:), take note of these two things:

  1. Both methods provide an ORKTaskResult. This object contains the current state of results from the task. In other words, it holds all the sub-results from the completed steps thus far.
  2. You therefore can use these results to construct other steps, or to make your decision about which step to show next.

The sub-result for the question "What is your name?" resides deep inside the ORKTaskResult, so copy this function into your class to dig it out and return the user's input as a String?:

func findName(result: ORKTaskResult) -> String? {  if let stepResult = result.resultForIdentifier(nameStepID) as? ORKStepResult, let subResults = stepResult.results, let textQuestionResult = subResults[0] as? ORKTextQuestionResult {    return textQuestionResult.textAnswer  }  return nil}

If all that if let unwrapping succeeds, you're left with a ORKTextQuestionResult, from which you access the textAnswer property and return the user's name.

With that function handy, you can now replace the lines in stepBeforeStep(_:withResult:) andstepAfterStep(_:withResult:) which currently read:

// TODOreturn questStep(nil)

with:


return questStep(findName(result))

Your new survey task is all done! The last thing you need to do is redirect your app to use this task. Go to ViewController.swift and find the line that reads:

let taskViewController = ORKTaskViewController(task: SurveyTask, taskRunUUID: nil)

and replace it with:

let taskViewController = ORKTaskViewController(task: CustomSurveyTask(), taskRunUUID: nil)

It's time to test! Build and Run your app, and open the survey. Fill in your name:

Screenshot

And then notice that the next question addresses you by that name...

Screenshot

Cool! So you've now built the capability for your survey's content and structure to react to user input. This example may seem small, but the possibilities are wide.

The Sound of Your Own Voice

The second upgrade to your app is the ability to play back the recording you are already creating withMicrophoneTask.swift.

Open ViewController.swift and look at taskViewController(_:didFinishWithReason:error:). Notice how you receive the ORKTaskViewController object that just finished.

The trick here is that an ORKTaskViewController object has a property named result which is anORKTaskResult. That's your ticket to processing the data your task produces.

Remember how you used findName(_:) in CustomSurveyTask to dig out the user's name from anORKTaskResult? You'll do something very similar here to find the sound file's URL. Copy this function into your view controller:

func findSoundFile(result: ORKTaskResult) -> NSURL? {  if let results = result.results {    if results.count > 3 {      if let stepResult = results[3] as? ORKStepResult, let subResults = stepResult.results, let fileResult = subResults[0] as? ORKFileResult {        return fileResult.fileURL      }    }  }  return nil}

Again, the ORKTaskResult has a very nested structure and it takes some digging to find the data you want. It may seem verbose now, but for a complicated task the structure stays nice and organized.

Now that you have a way to obtain the file URL, you add a way to play the sound back. Start at the top of ViewController.swift and add this import:

import AVFoundation

Then define two variables in the class:

var audioPlayer: AVAudioPlayer?var soundFileURL: NSURL?

With a var for the URL defined, now add this line insidetaskViewController(_:didFinishWithReason:error:):

soundFileURL = findSoundFile(taskViewController.result)

You're now checking the results of the task for a sound file and hanging on to its file location if found.

To play the sound, add this method to your view controller:

@IBAction func playMostRecentSound(sender: AnyObject) {  if let soundFileURL = soundFileURL {    audioPlayer = AVAudioPlayer(contentsOfURL: soundFileURL, fileTypeHint: AVFileTypeAppleM4A, error: nil)    audioPlayer?.play()  }}

Whenever a button is pressed, you check for an existing sound and play it if possible.

Finally, simply create a UIButton in your storyboard and call playMostRecentSound(_:) when the user taps it.

Build and Run, and you can try out your recording and playback! (I always think I sound nasally when I hear myself recorded, but I can get used to it.)

What Next?

You can see the completed source code here on GitHub. The two extra features from this article illustrate how you can make your ResearchKit surveys dynamic, and how to fetch any recorded audio samples. Some possible next steps to try:

  • If the user chooses a certain quest, ask a relevant follow-up question just for them.
  • Require the user to complete the consent section of the app and save the user's signature before the survey and microphone tasks become available.

Ultimately, it's up to you how you'd like to take it from here, and I hope you have a clearer idea of how to do so now. Thanks for reading and I hope to continue to see more ResearchKit apps!