Skip to content

Commit

Permalink
Add support for sliders (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
vishnuravi authored Aug 2, 2023
1 parent b357603 commit f728c67
Show file tree
Hide file tree
Showing 12 changed files with 171 additions and 10 deletions.
3 changes: 3 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@ only_rules:
# The variable should be placed on the left, the constant on the right of a comparison operator.
- yoda_condition

attributes:
attributes_with_arguments_always_on_line_above: false

deployment_target: # Availability checks or attributes shouldn’t be using older versions that are satisfied by the deployment target.
iOSApplicationExtension_deployment_target: 16.0
iOS_deployment_target: 16.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import SwiftUI


class QuestionnaireResponseStorage: ObservableObject {
@Published
private var responses: [URL: [QuestionnaireResponse]] = [:]
@Published private var responses: [URL: [QuestionnaireResponse]] = [:]


func append(_ response: QuestionnaireResponse, for identifier: URL) {
Expand Down
32 changes: 32 additions & 0 deletions Example/ExampleUITests/ExampleUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
// SPDX-License-Identifier: MIT
//

// We disable file length because this is a test
// swiftlint:disable file_length
import XCTest

// We disable type body length rule because this is a test
Expand Down Expand Up @@ -301,6 +303,36 @@ final class ExampleUITests: XCTestCase {
app.swipeDown(velocity: XCUIGestureVelocity.fast)
}

func testSliderExample() throws {
let app = XCUIApplication()
app.launch()

let sliderExampleButton = app.collectionViews.buttons["Slider Example"]

// Open questionnaire and start
sliderExampleButton.tap()

// Access the slider
let slider = app.sliders.firstMatch
XCTAssertTrue(slider.exists, "The slider does not exist")

// Calculate normalized position for the desired value
let desiredValue: CGFloat = 5
let sliderRange: CGFloat = 10
let normalizedPosition = desiredValue / sliderRange

// Adjust the slider's value
slider.adjust(toNormalizedSliderPosition: normalizedPosition)

// Check that the slider's value is now equal to the desired value
if let valueString = slider.value as? String, let value = Double(valueString) {
let sliderValue = CGFloat(value)
XCTAssertEqual(sliderValue, desiredValue, accuracy: 1)
} else {
XCTFail("Slider value is not a readable number.")
}
}

// MARK: UI Tests for Clinical Questionnaires

func testPHQ9Example() throws {
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ let package = Package(
.copy("Resources/IPSS.json"),
.copy("Resources/FormExample.json"),
.copy("Resources/MultipleEnableWhen.json"),
.copy("Resources/ImageCapture.json")
.copy("Resources/ImageCapture.json"),
.copy("Resources/SliderExample.json")
]
),
.testTarget(
Expand Down
6 changes: 5 additions & 1 deletion Sources/FHIRQuestionnaires/Questionnaire+Resources.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ extension Questionnaire {

/// A FHIR questionnaire demonstrating an image capture step
public static var imageCaptureExample: Questionnaire = loadQuestionnaire(withName: "ImageCapture")

/// A FHIR questionnaire demonstrating a slider
public static var sliderExample: Questionnaire = loadQuestionnaire(withName: "SliderExample")

/// A collection of example `Questionnaire`s provided by the FHIRQuestionnaires target to demonstrate functionality
public static var exampleQuestionnaires: [Questionnaire] = [
Expand All @@ -46,7 +49,8 @@ extension Questionnaire {
.dateTimeExample,
.formExample,
.multipleEnableWhen,
.imageCaptureExample
.imageCaptureExample,
.sliderExample
]

// MARK: Examples of clinical research FHIR Questionnaires
Expand Down
42 changes: 42 additions & 0 deletions Sources/FHIRQuestionnaires/Resources/SliderExample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"resourceType": "Questionnaire",
"language": "en-US",
"id": "Stanford University-slider-example",
"title": "Slider Example",
"status": "draft",
"publisher": "Stanford University",
"url": "http://biodesign.stanford.edu/questionnaires/sliderexample",
"item": [
{
"linkId": "1",
"text": "How bad is the pain on a scale of 0-10?",
"type": "integer",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-item-control",
"code": "slider",
"display": "Slider"
}
]
}
},
{
"url": "http://hl7.org/fhir/StructureDefinition/minValue",
"valueInteger": 0
},
{
"url": "http://hl7.org/fhir/StructureDefinition/maxValue",
"valueInteger": 10
},
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-sliderStepValue",
"valueInteger": 1
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

This source file is part of the ResearchKitOnFHIR open source project

SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)

SPDX-License-Identifier: MIT
24 changes: 24 additions & 0 deletions Sources/ResearchKitOnFHIR/FHIRExtensions/FHIRExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import ModelsR4
extension QuestionnaireItem {
/// Supported FHIR extensions for QuestionnaireItems
private enum SupportedExtensions {
static let itemControl = "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl"
static let questionnaireUnit = "http://hl7.org/fhir/StructureDefinition/questionnaire-unit"
static let regex = "http://hl7.org/fhir/StructureDefinition/regex"
static let sliderStepValue = "http://hl7.org/fhir/StructureDefinition/questionnaire-sliderStepValue"
static let validationMessage = "http://biodesign.stanford.edu/fhir/StructureDefinition/validationtext"
static let maxDecimalPlaces = "http://hl7.org/fhir/StructureDefinition/maxDecimalPlaces"
static let minValue = "http://hl7.org/fhir/StructureDefinition/minValue"
Expand All @@ -33,6 +35,17 @@ extension QuestionnaireItem {
}
return isHidden
}

/// Defines the control type for the answer for a question
/// - Returns: A code representing the control type (i.e. slider)
var itemControl: String? {
guard let itemControlExtension = getExtensionInQuestionnaireItem(url: SupportedExtensions.itemControl),
case let .codeableConcept(concept) = itemControlExtension.value,
let itemControlCode = concept.coding?.first?.code?.value?.string else {
return nil
}
return itemControlCode
}

/// The minimum value for a numerical answer.
/// - Returns: An optional `NSNumber` containing the minimum value allowed.
Expand Down Expand Up @@ -66,6 +79,17 @@ extension QuestionnaireItem {
}
return NSNumber(value: maxDecimalPlaces)
}

/// The offset between numbers on a numerical slider
/// - Returns: An optional `NSNumber` representing the size of each discrete offset on the scale.
var sliderStepValue: NSNumber? {
guard let sliderStepValueExtension = getExtensionInQuestionnaireItem(url: SupportedExtensions.sliderStepValue),
case let .integer(integerValue) = sliderStepValueExtension.value,
let sliderStepValue = integerValue.value?.integer as? Int32 else {
return nil
}
return NSNumber(value: sliderStepValue)
}

/// The unit of a quantity answer type.
/// - Returns: An optional `String` containing the unit (i.e. cm) if it was provided.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ extension QuestionnaireItem {
/// - Returns: An object of type `ORKAnswerFormat` representing the type of answer this question accepts.
private func toORKAnswerFormat(valueSets: [ValueSet]) throws -> ORKAnswerFormat {
// swiftlint:disable:previous cyclomatic_complexity
// We have to cover all the switch cases in the following statement driving up the overal comlexity.
// We have to cover all the switch cases in the following statement driving up the overall complexity.
switch type.value {
case .boolean:
return ORKBooleanAnswerFormat.booleanAnswerFormat()
Expand All @@ -165,6 +165,16 @@ extension QuestionnaireItem {
answerFormat.maximum = maxValue
return answerFormat
case .integer:
if itemControl == "slider" {
let answerFormat = ORKScaleAnswerFormat(
maximumValue: maxValue?.intValue ?? 0,
minimumValue: minValue?.intValue ?? 0,
defaultValue: minValue?.intValue ?? 0,
step: Int(truncating: sliderStepValue ?? 1)
)
return answerFormat
}

let answerFormat = ORKNumericAnswerFormat.integerAnswerFormat(withUnit: nil)
answerFormat.minimum = minValue
answerFormat.maximum = maxValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,18 @@ extension ORKTaskResult {
responseAnswer.value = createBooleanResponse(result)
case let result as ORKChoiceQuestionResult:
responseAnswer.value = createChoiceResponse(result)
case let result as ORKFileResult:
responseAnswer.value = createAttachmentResponse(result)
case let result as ORKNumericQuestionResult:
responseAnswer.value = createNumericResponse(result)
case let result as ORKDateQuestionResult:
responseAnswer.value = createDateResponse(result)
case let result as ORKTimeOfDayQuestionResult:
responseAnswer.value = createTimeResponse(result)
case let result as ORKScaleQuestionResult:
responseAnswer.value = createScaleResponse(result)
case let result as ORKTextQuestionResult:
responseAnswer.value = createTextResponse(result)
case let result as ORKFileResult:
responseAnswer.value = createAttachmentResponse(result)
case let result as ORKTimeOfDayQuestionResult:
responseAnswer.value = createTimeResponse(result)
default:
// Unsupported result type
responseAnswer.value = nil
Expand Down Expand Up @@ -92,7 +94,15 @@ extension ORKTaskResult {
return .decimal(FHIRPrimitive(FHIRDecimal(value.decimalValue)))
}
}


private func createScaleResponse(_ result: ORKScaleQuestionResult) -> QuestionnaireResponseItemAnswer.ValueX? {
guard let value = result.scaleAnswer else {
return nil
}

return .integer(FHIRPrimitive(FHIRInteger(value.int32Value)))
}

private func createTextResponse(_ result: ORKTextQuestionResult) -> QuestionnaireResponseItemAnswer.ValueX? {
guard let text = result.textAnswer else {
return nil
Expand Down
12 changes: 12 additions & 0 deletions Tests/ResearchKitOnFHIRTests/FHIRToResearchKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,25 @@ final class FHIRToResearchKitTests: XCTestCase {
let valueSets = Questionnaire.containedValueSetExample.getContainedValueSets()
XCTAssertEqual(valueSets.count, 1)
}

func testItemControlExtension() throws {
let testItemControl = Questionnaire.sliderExample.item?.first?.itemControl
let itemControlValue = try XCTUnwrap(testItemControl)
XCTAssertEqual(itemControlValue, "slider")
}

func testRegexExtension() throws {
let testRegex = Questionnaire.textValidationExample.item?.first?.validationRegularExpression
// swiftlint:disable:next line_length
let regex = try NSRegularExpression(pattern: "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
XCTAssertEqual(regex, testRegex)
}

func testSliderStepValueExtension() throws {
let testSliderStepValue = Questionnaire.sliderExample.item?.first?.sliderStepValue
let sliderStepValue = try XCTUnwrap(testSliderStepValue)
XCTAssertEqual(sliderStepValue, 1)
}

func testValidationMessageExtension() throws {
let testValidationMessage = Questionnaire.textValidationExample.item?.first?.validationMessage
Expand Down
18 changes: 18 additions & 0 deletions Tests/ResearchKitOnFHIRTests/ResearchKitToFHIRTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@ final class ResearchKitToFHIRTests: XCTestCase {
}
XCTAssertEqual(testValue, responseValue)
}

func testScaleResponse() {
let testValue = 1
var responseValue: Int?

let scaleResult = ORKScaleQuestionResult(identifier: "scaleResult")
scaleResult.scaleAnswer = testValue as NSNumber
let taskResult = createTaskResult(scaleResult)

let fhirResponse = taskResult.fhirResponse
let answer = fhirResponse.item?.first?.answer?.first?.value

if case let .integer(value) = answer,
let unwrappedValue = value.value?.integer {
responseValue = Int(unwrappedValue)
}
XCTAssertEqual(testValue, responseValue)
}

func testQuantityResponse() {
let testValue: Decimal = 1.5
Expand Down

0 comments on commit f728c67

Please sign in to comment.