diff --git a/PICS.xcodeproj/project.pbxproj b/PICS.xcodeproj/project.pbxproj index c533b1c..540cbc9 100644 --- a/PICS.xcodeproj/project.pbxproj +++ b/PICS.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 22365B782B902C9100C4528E /* Onboarding-Questionnaire.json in Resources */ = {isa = PBXBuildFile; fileRef = 22365B772B902C9100C4528E /* Onboarding-Questionnaire.json */; }; 226C16F02B69DF3500FBA97D /* HeightKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226C16EF2B69DF3500FBA97D /* HeightKey.swift */; }; 226C16F22B6C820C00FBA97D /* WeightKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226C16F12B6C820B00FBA97D /* WeightKey.swift */; }; + 22C75CDA2B969B89008986AF /* ReactionTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C75CD92B969B89008986AF /* ReactionTime.swift */; }; 22C75CE12B978E33008986AF /* Medication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C75CE02B978E33008986AF /* Medication.swift */; }; 22C75CF02B979D02008986AF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C75CEF2B979D02008986AF /* ContentView.swift */; }; 22C75CF72B979EA7008986AF /* ImageSource in Frameworks */ = {isa = PBXBuildFile; productRef = 22C75CF62B979EA7008986AF /* ImageSource */; }; @@ -140,6 +141,7 @@ 22365B772B902C9100C4528E /* Onboarding-Questionnaire.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "Onboarding-Questionnaire.json"; sourceTree = ""; }; 226C16EF2B69DF3500FBA97D /* HeightKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeightKey.swift; sourceTree = ""; }; 226C16F12B6C820B00FBA97D /* WeightKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeightKey.swift; sourceTree = ""; }; + 22C75CD92B969B89008986AF /* ReactionTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionTime.swift; sourceTree = ""; }; 22C75CE02B978E33008986AF /* Medication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Medication.swift; sourceTree = ""; }; 22C75CEF2B979D02008986AF /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 27FA298F2A388E9B009CAC45 /* ModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalView.swift; sourceTree = ""; }; @@ -478,6 +480,7 @@ 864909412B8AA1C300054C9A /* TrailMakingTest.swift */, A459932B2B906C3B00A98C95 /* ResultsViz.swift */, 74AE05872B918DAF00AB5287 /* StroopTest.swift */, + 22C75CD92B969B89008986AF /* ReactionTime.swift */, ); path = Assessment; sourceTree = ""; @@ -767,6 +770,7 @@ A480C7C02B6D5A3700B29A07 /* HKVisualization.swift in Sources */, 2F5E32BD297E05EA003432F8 /* PICSDelegate.swift in Sources */, A459932C2B906C3B00A98C95 /* ResultsViz.swift in Sources */, + 22C75CDA2B969B89008986AF /* ReactionTime.swift in Sources */, 2FE5DC5229EDD7FA004B9AB4 /* PICSScheduler.swift in Sources */, A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */, 22306A462B8FB173000A8EC1 /* OnboardingQuestionnaireDashboard.swift in Sources */, diff --git a/PICS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PICS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 56cecb1..c238f68 100644 --- a/PICS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/PICS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/ResearchKit", "state" : { - "revision" : "15f06cf7c1d2d22805b7b939823536bc78ad63a6", - "version" : "2.2.25" + "revision" : "e4afb10ec636a70e06afc4a84fb98e457818439a", + "version" : "2.2.27" } }, { diff --git a/PICS/Assessment/Assessments.swift b/PICS/Assessment/Assessments.swift index 44d2e94..a971928 100644 --- a/PICS/Assessment/Assessments.swift +++ b/PICS/Assessment/Assessments.swift @@ -24,6 +24,7 @@ struct Assessments: View { enum Assessments { case trailMaking case stroopTest + case reactionTime } // Binding to control the display of account-related UI. @@ -32,6 +33,7 @@ struct Assessments: View { // Local storage of results of the Trail Making and Stroop tests for test results for layerplotting, analysis etc. @AppStorage("trailMakingResults") private var tmStorageResults: [AssessmentResult] = [] @AppStorage("stroopTestResults") private var stroopTestResults: [AssessmentResult] = [] + @AppStorage("reactionTimeResults") private var reactionTimeResults: [AssessmentResult] = [] // Decide whether to show test or not. @AppStorage("AssessmentsInProgress") private var assessmentsIP = false // Tracks which test is currently selected. @@ -46,6 +48,8 @@ struct Assessments: View { .padding(10) stroopTestSection .padding(10) + reactionTimeSection + .padding(10) } } @@ -58,6 +62,8 @@ struct Assessments: View { TrailMakingTaskView() case .stroopTest: StroopTestView() + case .reactionTime: + ReactionTimeView() } } else { assessmentList @@ -76,6 +82,8 @@ struct Assessments: View { TrailMakingTaskView() case .stroopTest: StroopTestView() + case .reactionTime: + ReactionTimeView() } } } @@ -123,6 +131,28 @@ struct Assessments: View { } } } + private var reactionTimeSection: some View { + // Button text to start the ReactionTime Test or view results + // based on whether results are available. + let btnText = if reactionTimeResults.isEmpty { + String(localized: "ASSESSMENT_STROOP_START_BTN") + } else { + String(localized: "ASSESSMENT_RESULTS_BTN") + } + return Section { + VStack { + reactionTimeResultsView + Divider() + .padding(.bottom, 5) + Button(action: startReactionTimeTest) { + Text(btnText) + .foregroundStyle(.accent) + } + // Use style to restrict clickable area. + .buttonStyle(.plain) + } + } + } // Views for displaying results of Trail Making, or a message indicating the test has not been completed. private var trailMakingTestResultsView: some View { @@ -155,6 +185,21 @@ struct Assessments: View { } } } + // Views for displaying results of the ReactionTime test, or a message indicating the test has not been completed. + private var reactionTimeResultsView: some View { + Group { + if reactionTimeResults.isEmpty { + notCompletedView(testName: "Reaction Time Test") + } else { + ResultsViz( + data: reactionTimeResults, + xName: "Time", + yName: "Results", + title: String(localized: "REACTIONTIME_VIZ_TITLE") + ) + } + } + } // Initializes the view with a binding to control whether the account UI is being presented. init(presentingAccount: Binding) { @@ -174,6 +219,12 @@ struct Assessments: View { assessmentsIP = true showingTestSheet.toggle() } + // Function to set up and start the ReactionTime Test. + func startReactionTimeTest() { + currentTest = Assessments.reactionTime + assessmentsIP = true + showingTestSheet.toggle() + } // A view for displaying a message indicating that a specific assessment has not been completed. private func notCompletedView(testName: String) -> some View { diff --git a/PICS/Assessment/ReactionTime.swift b/PICS/Assessment/ReactionTime.swift new file mode 100644 index 0000000..d89906d --- /dev/null +++ b/PICS/Assessment/ReactionTime.swift @@ -0,0 +1,82 @@ +// +// This source file is part of the PICS based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import ResearchKit +import ResearchKitSwiftUI +import SwiftUI + +struct ReactionTimeView: View { + @AppStorage("reactionTimeResults") private var reactionTimeResults: [AssessmentResult] = [] + @AppStorage("AssessmentsInProgress") private var assessmentsIP = false + @Environment(\.presentationMode) var presentationMode + + var body: some View { + ZStack { + Color(red: 242 / 255, green: 242 / 255, blue: 247 / 255) + .edgesIgnoringSafeArea(.all) + // Displays the ResearchKit ordered task view for the ReactionTime Test. + ORKOrderedTaskView( + tasks: createReactionTimeTask(), + tintColor: .accentColor, + shouldConfirmCancel: true, + result: handleTaskResult + ) + } + } + + // Creates the ReactionTime task to be presented to the user + private func createReactionTimeTask() -> ORKOrderedTask { + // Initializes a ReactionTime task with the following specified parameters + let task = ORKOrderedTask.reactionTime( + withIdentifier: "ReactionTimeTask", + intendedUseDescription: nil, + maximumStimulusInterval: 2.0, + minimumStimulusInterval: 1.0, + thresholdAcceleration: 0.8, + numberOfAttempts: 1, + timeout: 5.0, + successSound: 0, + timeoutSound: 0, + failureSound: 0, + options: [.excludeConclusion] + ) + return task + } + // Handles the result of the ReactionTime task. + private func handleTaskResult(result: TaskResult) async { + let curTime = ProcessInfo.processInfo.systemUptime + assessmentsIP = false // End the assessment + // Adding this logic to dismiss the view + DispatchQueue.main.async { + self.presentationMode.wrappedValue.dismiss() + } + guard case let .completed(taskResult) = result else { + // Failed or canceled test. Do nothing for current. + return + } + // Fields to record the aggregated test results. + var totalTime: TimeInterval = 0 + // Extract and process the ReactionTime test results. + for result in taskResult.results ?? [] { + if let stepResult = result as? ORKStepResult, + stepResult.identifier == "reactionTime" { + for reactionTimeResult in stepResult.results ?? [] { + if let curResult = reactionTimeResult as? ORKReactionTimeResult { + // Calculates the total time taken to complete the test. + totalTime += curTime - curResult.timestamp + } + } + } + } + reactionTimeResults += [AssessmentResult(testDateTime: Date(), timeSpent: totalTime)] + } +} + +#Preview { + ReactionTimeView() +} diff --git a/PICS/Assessment/ResultsViz.swift b/PICS/Assessment/ResultsViz.swift index 547be60..8b722e6 100644 --- a/PICS/Assessment/ResultsViz.swift +++ b/PICS/Assessment/ResultsViz.swift @@ -158,6 +158,14 @@ struct ResultsViz: View { } else { elm.errorCnt } + let metricText = if metricNum < 0 { + "" + } else { + ", " + + self.metricType + + ": " + + String(metricNum) + } return ( prefix + String(elm.testDateTime.formatted(.dateTime.year().month().day())) + @@ -165,10 +173,7 @@ struct ResultsViz: View { self.timeSpentLable + ": " + String(timeSpent) + - ", " + - self.metricType + - ": " + - String(metricNum) + metricText ) } diff --git a/PICS/Resources/Localizable.xcstrings b/PICS/Resources/Localizable.xcstrings index d033162..bd6a507 100644 --- a/PICS/Resources/Localizable.xcstrings +++ b/PICS/Resources/Localizable.xcstrings @@ -978,6 +978,28 @@ } } }, + "REACTIONTIME_TEST" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reaction Time Test" + } + } + } + }, + "REACTIONTIME_VIZ_TITLE" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reaction Time Results" + } + } + } + }, "Repository Link" : { "localizations" : { "en" : {