Building Surveys using Xcode

In this article by Dhanushram Balachandran and Edward Cessna author of book Getting Started with ResearchKit, you can find the Softwareitis.xcodeproj project in the Chapter_3/Softwareitis folder of the RKBook GitHub repository (https://github.com/dhanushram/RKBook/tree/master/Chapter_3/Softwareitis).

(For more resources related to this topic, see here.)

Now that you have learned about the results of tasks from the previous section, we can modify the Softwareitis project to incorporate processing of the task results. In the TableViewController.swift file, let's update the rows data structure to include the reference for processResultsMethod: as shown in the following:

//Array of dictionaries. Each dictionary contains [ rowTitle : (didSelectRowMethod, processResultsMethod) ]
var rows : [ [String : ( didSelectRowMethod:()->(), processResultsMethod:(ORKTaskResult?)->() )] ] = []

Update the ORKTaskViewControllerDelegate method taskViewController(taskViewController:, didFinishWithReason:, error:) in TableViewController to call processResultsMethod, as shown in the following:

func taskViewController(taskViewController: ORKTaskViewController, didFinishWithReason reason: ORKTaskViewControllerFinishReason, error: NSError?)
{
   if let indexPath = tappedIndexPath
   {
       //1
       let rowDict = rows[indexPath.row]
       if let tuple = rowDict.values.first
       {
           //2
           tuple.processResultsMethod(taskViewController.result)
       }
   }
   dismissViewControllerAnimated(true, completion: nil)
}
  1. Retrieves the dictionary of the tapped row and its associated tuple containing the didSelectRowMethod and processResultsMethod references from rows.
  2. Invokes the processResultsMethod with taskViewController.result as the parameter.

Now, we are ready to create our first survey. In Survey.swift, under the Surveys folder, you will find two methods defined in the TableViewController extension: showSurvey() and processSurveyResults(). These are the methods that we will be using to create the survey and process the results.

Instruction step

Instruction step is used to show instruction or introductory content to the user at the beginning or middle of a task. It does not produce any result as its an informational step. We can create an instruction step using the ORKInstructionStep object. It has title and detailText properties to set the appropriate content. It also has the image property to show an image.

The ORKCompletionStep is a special type of ORKInstructionStep used to show the completion of a task. The ORKCompletionStep shows an animation to indicate the completion of the task along with title and detailText, similar to ORKInstructionStep.

In creating our first Softwareitis survey, let's use the following two steps to show the information:

func showSurvey()
{
   //1
   let instStep = ORKInstructionStep(identifier: "Instruction Step")
   instStep.title = "Softwareitis Survey"
   instStep.detailText = "This survey demonstrates different question types."
   //2
   let completionStep = ORKCompletionStep(identifier: "Completion Step")
   completionStep.title = "Thank you for taking this survey!"
   //3
   let task = ORKOrderedTask(identifier: "first survey", steps: [instStep, completionStep])
   //4
   let taskViewController = ORKTaskViewController(task: task, taskRunUUID: nil)
   taskViewController.delegate = self
   presentViewController(taskViewController, animated: true, completion: nil)
}

The explanation of the preceding code is as follows:

  1. Creates an ORKInstructionStep object with an identifier "Instruction Step" and sets its title and detailText properties.
  2. Creates an ORKCompletionStep object with an identifier "Completion Step" and sets its title property.
  3. Creates an ORKOrderedTask object with the instruction and completion step as its parameters.
  4. Creates an ORKTaskViewController object with the ordered task that was previously created and presents it to the user.

Let's update the processSurveyResults method to process the results of the instruction step and the completion step as shown in the following:

func processSurveyResults(taskResult: ORKTaskResult?)
{
   if let taskResultValue = taskResult
   {
       //1
       print("Task Run UUID : " + taskResultValue.taskRunUUID.UUIDString)
       print("Survey started at : \(taskResultValue.startDate!)     Ended at : \(taskResultValue.endDate!)")
       //2
       if let instStepResult = taskResultValue.stepResultForStepIdentifier("Instruction Step")
       {
           print("Instruction Step started at : \(instStepResult.startDate!)   Ended at : \(instStepResult.endDate!)")
       }
       //3
       if let compStepResult = taskResultValue.stepResultForStepIdentifier("Completion Step")
       {
           print("Completion Step started at : \(compStepResult.startDate!)   Ended at : \(compStepResult.endDate!)")
       }
   }
}

The explanation of the preceding code is given in the following:

  1. As mentioned at the beginning, each task run is associated with a UUID. This UUID is available in the taskRunUUID property, which is printed in the first line. The second line prints the start and end date of the task. These are useful user analytics data with regards to how much time the user took to finish the survey.
  2. Obtains the ORKStepResult object corresponding to the instruction step using the stepResultForStepIdentifier method of the ORKTaskResult object. Prints the start and end date of the step result, which shows the amount of time for which the instruction step was shown before the user pressed the Get Started or Cancel buttons. Note that, as mentioned earlier, ORKInstructionStep does not produce any results. Therefore, the results property of the ORKStepResult object will be nil. You can use a breakpoint to stop the execution at this line of code and verify it.
  3. Obtains the ORKStepResult object corresponding to the completion step. Similar to the instruction step, this prints the start and end date of the step.

The preceding code produces screens as shown in the following image:

After the Done button is pressed in the completion step, Xcode prints the output that is similar to the following:

Task Run UUID : 0A343E5A-A5CD-4E7C-88C6-893E2B10E7F7
Survey started at : 2015-08-11 00:41:03 +0000     Ended at : 2015-08-11 00:41:07 +0000
Instruction Step started at : 2015-08-11 00:41:03 +0000   Ended at : 2015-08-11 00:41:05 +0000
Completion Step started at : 2015-08-11 00:41:05 +0000   Ended at : 2015-08-11 00:41:07 +0000

Question step

Question steps make up the body of a survey. ResearchKit supports question steps with various answer types such as boolean (Yes or No), numeric input, date selection, and so on.

Let's first create a question step with the simplest boolean answer type by inserting the following line of code in showSurvey():

let question1 = ORKQuestionStep(identifier: "question 1", title: "Have you ever been diagnosed with Softwareitis?", answer: ORKAnswerFormat.booleanAnswerFormat())

The preceding code creates a ORKQuestionStep object with identifier question 1, title with the question, and an ORKBooleanAnswerFormat object created using the booleanAnswerFormat() class method of ORKAnswerFormat. The answer type for a question is determined by the type of the ORKAnswerFormat object that is passed in the answer parameter. The ORKAnswerFormat has several subclasses such as ORKBooleanAnswerFormat, ORKNumericAnswerFormat, and so on. Here, we are using ORKBooleanAnswerFormat.

Don't forget to insert the created question step in the ORKOrderedTask steps parameter by updating the following line:

let task = ORKOrderedTask(identifier: "first survey", steps: [instStep, question1, completionStep])

When you run the preceding changes in Xcode and start the survey, you will see the question step with the Yes or No options. We have now successfully added a boolean question step to our survey, as shown in the following image:

Now, its time to process the results of this question step. The result is produced in an ORKBooleanQuestionResult object. Insert the following lines of code in processSurveyResults():

//1
if let question1Result = taskResultValue.stepResultForStepIdentifier("question 1")?.results?.first as? ORKBooleanQuestionResult
{
   //2
   if question1Result.booleanAnswer != nil
   {
       let answerString = question1Result.booleanAnswer!.boolValue ? "Yes" : "No"
       print("Answer to question 1 is \(answerString)")
   }
   else
   {
       print("question 1 was skipped")
   }
}

The explanation of the preceding code is as follows:

  1. Obtains the ORKBooleanQuestionResult object by first obtaining the step result using the stepResultForStepIdentifier method, accessing its results property, and finally obtaining the only ORKBooleanQuestionResult object available in the results array.
  2. The booleanAnswer property of ORKBooleanQuestionResult contains the user's answer. We will print the answer if booleanAnswer is non-nil. If booleanAnswer is nil, it indicates that the user has skipped answering the question by pressing the Skip this question button.

    You can disable the skipping-of-a-question step by setting its optional property to false.

We can add the numeric and scale type question steps using the following lines of code in showSurvey():

//1
let question2 = ORKQuestionStep(identifier: "question 2", title: "How many apps do you download per week?", answer: ORKAnswerFormat.integerAnswerFormatWithUnit("Apps per week"))
//2
let answerFormat3 = ORKNumericAnswerFormat.scaleAnswerFormatWithMaximumValue(10, minimumValue: 0, defaultValue: 5, step: 1, vertical: false, maximumValueDescription: nil, minimumValueDescription: nil)
let question3 = ORKQuestionStep(identifier: "question 3", title: "How many apps do you download per week (range)?", answer: answerFormat3)

The explanation of the preceding code is as follows:

  1. Creates ORKQuestionStep with the ORKNumericAnswerFormat object, created using the integerAnswerFormatWithUnit method with Apps per week as the unit. Feel free to refer to the ORKNumericAnswerFormat documentation for decimal answer format and other validation options that you can use.
  2. First creates ORKScaleAnswerFormat with minimum and maximum values and step. Note that the number of step increments required to go from minimumValue to maximumValue cannot exceed 10. For example, maximum value of 100 and minimum value of 0 with a step of 1 is not valid and ResearchKit will raise an exception. The step needs to be at least 10. In the second line, ORKScaleAnswerFormat is fed in the ORKQuestionStep object.

The following lines in processSurveyResults() process the results from the number and the scale questions:

//1
if let question2Result = taskResultValue.stepResultForStepIdentifier("question 2")?.results?.first as? ORKNumericQuestionResult
{
     if question2Result.numericAnswer != nil
     {
         print("Answer to question 2 is \(question2Result.numericAnswer!)")
     }
     else
     {
         print("question 2 was skipped")
     }
}
//2
if let question3Result = taskResultValue.stepResultForStepIdentifier("question 3")?.results?.first as? ORKScaleQuestionResult
{
     if question3Result.scaleAnswer != nil
     {
         print("Answer to question 3 is \(question3Result.scaleAnswer!)")
     }
     else
     {
         print("question 3 was skipped")
     }
}

The explanation of the preceding code is as follows:

  1. Question step with ORKNumericAnswerFormat generates the result with the ORKNumericQuestionResult object. The numericAnswer property of ORKNumericQuestionResult contains the answer value if the question is not skipped by the user.
  2. The scaleAnswer property of ORKScaleQuestionResult contains the answer for a scale question.

As you can see in the following image, the numeric type question generates a free form text field to enter the value, while scale type generates a slider:

Let's look at a slightly complicated question type with ORKTextChoiceAnswerFormat. In order to use this answer format, we need to create the ORKTextChoice objects before hand. Each text choice object provides the necessary data to act as a choice in a single choice or multiple choice question. The following lines in showSurvey() create a single choice question with three options:

//1
let textChoice1 = ORKTextChoice(text: "Games", detailText: nil, value: 1, exclusive: false)
let textChoice2 = ORKTextChoice(text: "Lifestyle", detailText: nil, value: 2, exclusive: false)
let textChoice3 = ORKTextChoice(text: "Utility", detailText: nil, value: 3, exclusive: false)
//2
let answerFormat4 = ORKNumericAnswerFormat.choiceAnswerFormatWithStyle(ORKChoiceAnswerStyle.SingleChoice, textChoices: [textChoice1, textChoice2, textChoice3])
let question4 = ORKQuestionStep(identifier: "question 4", title: "Which category of apps do you download the most?", answer: answerFormat4)

The explanation of the preceding code is as follows:

  1. Creates text choice objects with text and value. When a choice is selected, the object in the value property is returned in the corresponding ORKChoiceQuestionResult object. The exclusive property is used in multiple choice questions context. Refer to the documentation for its use.
  2. First, creates an ORKChoiceAnswerFormat object with the text choices that were previously created and specifies a single choice type using the ORKChoiceAnswerStyle enum. You can easily change this question to multiple choice question by changing the ORKChoiceAnswerStyle enum to multiple choice. Then, an ORKQuestionStep object is created using the answer format object.

Processing the results from a single or multiple choice question is shown in the following. Needless to say, this code goes in the processSurveyResults() method:

//1
if let question4Result = taskResultValue.stepResultForStepIdentifier("question 4")?.results?.first as? ORKChoiceQuestionResult
{
     //2
     if question4Result.choiceAnswers != nil
     {
         print("Answer to question 4 is \(question4Result.choiceAnswers!)")
     }
     else
     {
         print("question 4 was skipped")
     }
}

The explanation of the preceding code is as follows:

  1. The result for a single or multiple choice question is returned in an ORKChoiceQuestionResult object.
  2. The choiceAnswers property holds the array of values for the chosen options.

The following image shows the generated choice question UI for the preceding code:

There are several other question types, which operate in a very similar manner like the ones we discussed so far. You can find them in the documentations of ORKAnswerFormat and ORKResult classes. The Softwareitis project has implementation of two additional types: date format and time interval format.

Using custom tasks, you can create surveys that can skip the display of certain questions based on the answers that the users have provided so far. For example, in a smoking habits survey, if the user chooses "I do not smoke" option, then the ability to not display the "How many cigarettes per day?" question.

Form step

A form step allows you to combine several related questions in a single scrollable page and reduces the number of the Next button taps for the user. The ORKFormStep object is used to create the form step. The questions in the form are represented using the ORKFormItem objects. The ORKFormItem is similar to ORKQuestionStep, in which it takes the same parameters (title and answer format).

Let's create a new survey with a form step by creating a form.swift extension file and adding the form entry to the rows array in TableViewController.swift, as shown in the following:

func setupTableViewRows()

{
     rows += [
         ["Survey" : (didSelectRowMethod: self.showSurvey, processResultsMethod: self.processSurveyResults)],
         //1
         ["Form" : (didSelectRowMethod: self.showForm, processResultsMethod: self.processFormResults)]
             ]
}

The explanation of the preceding code is as follows:

  1. The "Form" entry added to the rows array to create a new form survey with the showForm() method to show the form survey and the processFormResults() method to process the results from the form.

The following code shows the showForm() method in Form.swift file:

func showForm()
{
   //1
   let instStep = ORKInstructionStep(identifier: "Instruction Step")
   instStep.title = "Softwareitis Form Type Survey"
   instStep.detailText = "This survey demonstrates a form type step."
   //2
   let question1 = ORKFormItem(identifier: "question 1", text: "Have you ever been diagnosed with Softwareitis?", answerFormat: ORKAnswerFormat.booleanAnswerFormat())
   let question2 = ORKFormItem(identifier: "question 2", text: "How many apps do you download per week?", answerFormat: ORKAnswerFormat.integerAnswerFormatWithUnit("Apps per week"))
   //3
   let formStep = ORKFormStep(identifier: "form step", title: "Softwareitis Survey", text: nil)
   formStep.formItems = [question1, question2]
   //1
   let completionStep = ORKCompletionStep(identifier: "Completion Step")
   completionStep.title = "Thank you for taking this survey!"
   //4
   let task = ORKOrderedTask(identifier: "survey with form", steps: [instStep, formStep, completionStep])
   let taskViewController = ORKTaskViewController(task: task, taskRunUUID: nil)
   taskViewController.delegate = self
   presentViewController(taskViewController, animated: true, completion: nil)
}

The explanation of the preceding code is as follows:

  1. Creates an instruction and a completion step, similar to the earlier survey.
  2. Creates two ORKFormItem objects using the questions from the earlier survey. Notice the similarity with the ORKQuestionStep constructors.
  3. Creates ORKFormStep object with an identifier form step and sets the formItems property of the ORKFormStep object with the ORKFormItem objects that are created earlier.
  4. Creates an ordered task using the instruction, form, and completion steps and presents it to the user using a new ORKTaskViewController object.

The results are processed using the following processFormResults() method:

func processFormResults(taskResult: ORKTaskResult?)
{
   if let taskResultValue = taskResult
   {
       //1
       if let formStepResult = taskResultValue.stepResultForStepIdentifier("form step"), formItemResults = formStepResult.results
       {
           //2
           for result in formItemResults
           {
               //3
               switch result
               {
               case let booleanResult as ORKBooleanQuestionResult:
                   if booleanResult.booleanAnswer != nil
                   {
                       let answerString = booleanResult.booleanAnswer!.boolValue ? "Yes" : "No"
                       print("Answer to \(booleanResult.identifier) is \(answerString)")
                   }
                    else
                   {
                       print("\(booleanResult.identifier) was skipped")
                   }
               case let numericResult as ORKNumericQuestionResult:
                   if numericResult.numericAnswer != nil
                   {
                       print("Answer to \(numericResult.identifier) is \(numericResult.numericAnswer!)")
                   }
                   else
                   {
                       print("\(numericResult.identifier) was skipped")
                   }
               default: break
               }
           }
       }
   }
}

The explanation of the preceding code is as follows:

  1. Obtains the ORKStepResult object of the form step and unwraps the form item results from the results property.
  2. Iterates through each of the formItemResults, each of which will be the result for a question in the form.
  3. The switch statement detects the different types of question results and accesses the appropriate property that contains the answer.

The following image shows the form step:

Considerations for real world surveys

Many clinical research studies that are conducted using a pen and paper tend to have well established surveys. When you try to convert these surveys to ResearchKit, they may not convert perfectly. Some questions and answer choices may have to be reworded so that they can fit on a phone screen. You are advised to work closely with the clinical researchers so that the changes in the surveys still produce comparable results with their pen and paper counterparts. Another aspect to consider is to eliminate some of the survey questions if the answers can be found elsewhere in the user's device. For example, age, blood type, and so on, can be obtained from HealthKit if the user has already set them. This will help in improving the user experience of your app.

Summary

Here we have learned to build surveys using Xcode.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Getting Started with ResearchKit

Explore Title