Advanced survey navigation with ResearchKit

Monday, 14 March 2016 in dev journal by Vincent Tourraine

Designing surveys with ResearchKit can be as simple or as complex as you need it to be.

Our previous article gave a general presentation about how to create a survey, now we want to focus on navigation. In particular, how to build non-linear surveys with ResearchKit, with custom and dynamic navigation rules.

Ordered task

The most simple solution to build a survey is to use ORKOrderedTask (documentation), and setup a linear sequence of steps. All it needs is an array of ORKStep when you initialize it.

let steps = [introStep, step1, step2, completionStep]
let task = ORKOrderedTask(identifier: "...", steps: steps)

To configure non-linear surveys, ResearchKit provides ORKNavigableOrderedTask (documentation).

As the name suggests, it’s actually a subclass of ORKOrderedTask, so you initialize it with an array of steps. But this class also manages “step navigation rules” (ORKStepNavigationRule, documentation). These rules describe how a particular step can lead to another particular step, and under which conditions.

You can add a new rule with setNavigationRule:forTriggerStepIdentifier:. The “trigger” step identifier must refer to a step already in the steps array. The rule will be evaluated after the trigger step, to figure out what the next step should be. There can only be one rule by trigger step identifier (otherwise the controller wouldn’t know which rule to apply), if you call this method a second time with the same step identifier, the new rule will simply overwrite the previous rule. You can remove a rule for a given step identifier with removeNavigationRuleForTriggerStepIdentifier:. Existing rules can be fetched with navigationRuleForTriggerStepIdentifier: (this method returns nil if there’s no rule associated with that step). Finally, you can access all the existing rules with the stepNavigationRules dictionary property (the trigger step identifiers are the keys, the rules are the associated values).

You don’t need to specify a rule for every step. The initial steps array acts as a fallback order. So if the controller doesn’t have a rule for a particular step, it simply presents the next item from the steps array.

Direct navigation

The first type of rule is ORKDirectStepNavigationRule (documentation). This is a literally straightforward, non-conditional navigation, taking a single parameter: the destination step identifier.

Here’s an example of survey with a direct step navigation rule. There are 3 steps: questionA, questionB, and questionC. Then we configure a direct navigation from questionA to questionC.

let questionA = ORKQuestionStep(identifier: "questionA")
questionA.answerFormat = ORKBooleanAnswerFormat()
let questionB = ORKQuestionStep(identifier: "questionB")
questionB.answerFormat = ORKBooleanAnswerFormat()
let questionC = ORKQuestionStep(identifier: "questionC")
questionC.answerFormat = ORKBooleanAnswerFormat()

let steps = [questionA, questionB, questionC]
let task = ORKNavigableOrderedTask(identifier: "task", steps: steps)

let rule = ORKDirectStepNavigationRule(destinationStepIdentifier: questionC.identifier)
task.setNavigationRule(rule, forTriggerStepIdentifier: questionA.identifier)

When the participant completes questionA, the survey jumps to questionC. There’s no rule associated with questionC, and it’s the last item in the steps array, so the survey stops after completing this step, thus ignoring questionB.

Direct step rule navigation

This direct navigation might not be very useful on its own, but it’s frequently used in combination with other conditional rules, for instance to circle back to a particular question after a specific rule.

Predicate navigation

The second type of navigation rule is ORKPredicateStepNavigationRule (documentation).

You initialize it with one or multiple predicate(s), associated with destination step(s). These predicates can evaluate the results from the last step, or even a combination of results from multiple steps.

You need to use the generic NSPredicate class, but ORKResultPredicate (documentation) offers a wide range of convenience methods to easily build predicates based on ResearchKit results classes (“expected answer”, “minimum expected answer value”, and so on).

As an example, we can reuse the questions A, B, and C from the last sample code, and set a predicate step navigation rule instead of the direct navigation. We’ll test the result from questionA, and jump to questionC only if it’s positive. Otherwise, the default navigation principles apply, and the participant proceeds to questionB.

let predicate = ORKResultPredicate.predicateForBooleanQuestionResultWithResultSelector(
  ORKResultSelector(resultIdentifier: questionA.identifier), expectedAnswer: true)
let rule = ORKPredicateStepNavigationRule(
  resultPredicatesAndDestinationStepIdentifiers: [(predicate, questionC.identifier)])
task.setNavigationRule(rule, forTriggerStepIdentifier: questionA.identifier)

(These detailed classes and methods names are difficult to read, but they’re fairly self-explanatory, and can express complex rules with just a few lines of code.)

Predicate step rule navigation

Note that you can set an optional defaultStepIdentifier for the navigation rule, to give a fallback destination in case of no matching predicate. You can also assign additionalTaskResults to perform the predicate on results from another task. Several predicates can be combined using NSCompoundPredicate, to express “and”/“or”/“not” logical statements.

Loops and backward navigation

These rules make it possible to jump back to a previous step, or even repeat a sequence of steps any number of times. Keep in mind that doing so simply overwrite previous results, “correcting” existing results.

If you’re trying to collect multiple results for the same question, you will need to actually duplicate these steps, and make sure that they all have a distinct identifier.

Custom task

Navigable ordered task is extremely flexible, and should accommodate the vast majority of surveys structures. But if the various predicates don’t fit your needs, you can implement your own ORKTask subclass (documentation).

The most important method that you need to provide is stepAfterStep:withResult:. As you can guess, it should return the next step, based on the current step and its results (and any other custom value that you choose to manage with your class). You also need to implement stepBeforeStep:withResult:, to let the user navigate backward (or return nil, to prevent the participant from navigating back to a previous step).

Custom tasks require a significant amount of work, but they let you fully control the survey progress. ResearchKit is flexible enough to accommodate any type of question, with any type of navigation.