Closed InfuriatingYetti closed 1 year ago
In ResearchKit, each step in a survey will generate a particular result type. The result type in the WWDC sample app is different than the one you're dealing with here, so there will be a few extra steps needed to convert that to an OCKOutcomeValue.
The first step is to extract the selected text choices from the result. You're just about there in the snippet above.
let selectedChoices = results
.first { $0.identifier == ChoiceQuestion1Step }?
.choiceAnswers as! [ORKTextChoice]
The next step is to convert each selected choice to an outcome value.
let outcomesValues = selectedChoices
.map { OCKOutcomeValue($0.value as! String) }
Let me know if that works for you!
HI Gavi!
Thank you for getting back to me (it really means a lot!!), you're from the WWDC 2020 What's New in CareKit - so awesome, it's a pleasure and thank you for the insight! I'll give this a try and see what happens.
Out of curiosity, does the suggestion you provided use the string value (seen below: "None", "Slight", "Moderate", "Severe") or number value associated (ie. value: 0,1,2,3) with the ORKTextChoice questions (.singleChoice )? I'm sorry if I wasn't direct or as I didn't specifically say that, what I was looking to do is use the value of the response for insights, so in the example above "3" would be what I want to track.
For example, when looking at the answer choices:
Choices = [
ORKTextChoice(text: "None", value: 0 as NSNumber),
ORKTextChoice(text: "Slight", value: 1 as NSNumber),
ORKTextChoice(text: "Moderate", value: 2 as NSNumber),
ORKTextChoice(text: "Severe", value: 3 as NSNumber),`
....if the person taking the assessment selects "Severe", value: 3, I want the "3" to be what is extracted from the results to be persisted to the CareKit Store for creating insights. Is that what your suggestion will do? Is it possible to do this??
Thank you!
Ah I see! Yep that's possible as well. If the value is an NSNumber
make sure to convert it to a double
before storing it as an outcome value, as NSNumber
doesn't conform to OCKOutcomeValueUnderlyingType
. This is the current list of types that can be stored as outcome values:
/// Any value that can be persisted to `OCKStore` must conform to this protocol.
public protocol OCKOutcomeValueUnderlyingType: Codable {}
extension Int: OCKOutcomeValueUnderlyingType {}
extension Double: OCKOutcomeValueUnderlyingType {}
extension Bool: OCKOutcomeValueUnderlyingType {}
extension String: OCKOutcomeValueUnderlyingType {}
extension Data: OCKOutcomeValueUnderlyingType {}
extension Date: OCKOutcomeValueUnderlyingType {}
To convert an NSNumber
to a double
, you can write Double(truncating: nsNumberValue)
I hope I'm doing this right, I tried:
var choiceValue = OCKOutcomeValue(Double(truncating: choiceAnswer))
This gave me the line error of:
Cannot convert value of type '[any NSCoding & NSCopying & NSObjectProtocol]' to expected argument type 'NSNumber'
I feel that I might not be doing something right...
You're very close! It's a tricky process. The value
should be cast to the same type that was used when initially creating the ORKTask
with text choices. So if you created the text choices like so using NSNumber
s:
ORKTextChoice(text: "None", value: 0 as NSNumber)
Then you'll need to convert the values to NSNumbers
when they appear in the ORKTaskResult
:
let selectedChoices = results
.first { $0.identifier == ChoiceQuestion1Step }?
.choiceAnswers as! [ORKTextChoice]
let outcomesValues = selectedChoices
// Convert the choice values to `NSNumber`s
.map { $0.value as! NSNumber }
// Convert the choice values to CareKit outcome values
.map { OCKOutcomeValue(Double(truncating: $0)) }
This is great, and you're right it is tricky! I feel like I'm so close I can smell it ! :)
This part isn’t throwing an error:
let choiceAnswer = scale2Results
.first(where: { $0.identifier == choiceQuestion1Step })?
.choiceAnswers as? [ORKTextChoice],
I'm getting some errors at this part, I tried integrating what you suggested (I think correctly):
let outcomesValues = selectedChoices
// Convert the choice values to `NSNumber`s
.map { $0.value as! NSNumber }
// Convert the choice values to CareKit outcome values
as:
static func extractAnswersFromChoice(
_ result: ORKTaskResult) -> [OCKOutcomeValue]? {
guard
let choiceResults = result.results?
// Convert the choice values to `NSNumber`s
.map { $0.value as! NSNumber }
// Convert the choice values to CareKit outcome values
.map { OCKOutcomeValue(Double(truncating: $0)) },
Errors:
In all it looks like:
//
//Surveys.swift
//Recover
import CareKitStore
import ResearchKit
struct Choice {
static let choiceIdentifier = “choice”
static let choiceFormIdentifier = “choice.form"
static let ChoiceQuestion1Step = "checkin.form.choicequestion1"
static func Choice() -> ORKTask {
let Choices = [
ORKTextChoice(text: "None", value: 0 as NSNumber),
ORKTextChoice(text: "Slight", value: 1 as NSNumber),
ORKTextChoice(text: "Moderate", value: 2 as NSNumber),
ORKTextChoice(text: "Severe", value: 3 as NSNumber),
let choiceAnswerFormat = ORKValuePickerAnswerFormat.choiceAnswerFormat(
with: .singleChoice,
textChoices: Choices
)
let choiceStep1 = ORKFormItem(
identifier: ChoiceQuestion1Step,
text: "Question Here",
answerFormat: choiceAnswerFormat
)
choiceStep1.isOptional = false
static func extractAnswersFromChoice(
_ result: ORKTaskResult) -> [OCKOutcomeValue]? {
guard
//the part you suggested here:
let choiceResults = result.results?
// Convert the choice values to `NSNumber`s
.map { $0.value as! NSNumber }
// Convert the choice values to CareKit outcome values
.map { OCKOutcomeValue(Double(truncating: $0)) },
//part you suggested here
let choiceAnswer = scale2Results
.first(where: { $0.identifier == ChoiceQuestion1Step })?
.choiceAnswers as? [ORKTextChoice]
else {
assertionFailure("Failed to extract answers from check in survey!")
return nil
}
var choiceValue = OCKOutcomeValue(NSNumber(pointer: choiceAnswer) as! OCKOutcomeValueUnderlyingType)
choiceValue.kind = ChoiceQuestion1Step
return [choiceValue]
I also tried:
static func extractAnswersFromChoiceSurvey(
_ result: ORKTaskResult) -> [OCKOutcomeValue]? {
guard
let choiceResults = result.results?
.compactMap({ $0 as? ORKStepResult })
.first(where: { $0.identifier == choiceFormIdentifier }),
let scale2Results = choiceResults
.results?.compactMap({ $0 as? ORKChoiceQuestionResult }),
let choiceAnswer = scale2Results
.first(where: { $0.identifier == ChoiceQuestion1Step })?
.choiceAnswers as? [ORKTextChoice]
else {
assertionFailure("Failed to extract answers from check in survey!")
return nil
}
let outcomesValues = choiceAnswer
.map { OCKOutcomeValue($0.value as! String) }
var choiceValue = OCKOutcomeValue(NSNumber(pointer: outcomesValues) as! OCKOutcomeValueUnderlyingType)
choiceValue.kind = ChoiceQuestion1Step
This worked, as in it complied and ran the app; however, in the end it crashed and gave the error Thread 1: Fatal error: Failed to extract answers from check in survey!
Am I still on the right track...I apologize if it is something obvious!
Thank you!
Hi @gavirawson-apple!
I wanted to add to the above post. I have Aspergers and sometimes struggle explaining things concisely and in ways others understand. I'm not sure if the last post made sense, and I think I have the code added in all of the right places now and am getting some errors with the solutions provided (not sure about the last post, I think the code from the last post may not have been accurate).
In the photos below I start from where I have just added the suggestion, I show the error, correct it, and lastly (on no. 4) show the error from trying it out:
1.
2.
This is after I correct errors seen in #1-3 and run the app in the simulator, select ORKTextChoice(text: "Mild", value: 2 as NSNumber),
and click done:
4.
Is it possible that it has something to do with the CareFeedView Controller as well?? I'm not getting an error there, and this is what it looks like:
Thank you!!
PS - I have added the full code below of the Surveys.swift file to compliment the photos, in case it helps for big picture context:
//
//Surveys.swift
//
import CareKitStore
import ResearchKit
struct Surveys {
private init() {}
// MARK: Onboarding
static func onboardingSurvey() -> ORKTask {
// The Welcome Instruction step.
let welcomeInstructionStep = ORKInstructionStep(
identifier: "onboarding.welcome"
)
welcomeInstructionStep.title = "Welcome!"
welcomeInstructionStep.detailText = "Thank you for joining our study. Tap Next to learn more before signing up."
welcomeInstructionStep.image = UIImage(named: "welcome-image")
welcomeInstructionStep.imageContentMode = .scaleAspectFill
// The Informed Consent Instruction step.
let studyOverviewInstructionStep = ORKInstructionStep(
identifier: "onboarding.overview"
)
studyOverviewInstructionStep.title = "Before You Join"
studyOverviewInstructionStep.iconImage = UIImage(systemName: "checkmark.seal.fill")
let heartBodyItem = ORKBodyItem(
text: "The study will ask you to share some of your health data.",
detailText: nil,
image: UIImage(systemName: "heart.fill"),
learnMoreItem: nil,
bodyItemStyle: .image
)
let completeTasksBodyItem = ORKBodyItem(
text: "You will be asked to complete various tasks over the duration of the study.",
detailText: nil,
image: UIImage(systemName: "checkmark.circle.fill"),
learnMoreItem: nil,
bodyItemStyle: .image
)
let signatureBodyItem = ORKBodyItem(
text: "Before joining, we will ask you to sign an informed consent document.",
detailText: nil,
image: UIImage(systemName: "signature"),
learnMoreItem: nil,
bodyItemStyle: .image
)
let secureDataBodyItem = ORKBodyItem(
text: "Your data is kept private and secure.",
detailText: nil,
image: UIImage(systemName: "lock.fill"),
learnMoreItem: nil,
bodyItemStyle: .image
)
studyOverviewInstructionStep.bodyItems = [
heartBodyItem,
completeTasksBodyItem,
signatureBodyItem,
secureDataBodyItem
]
// The Signature step (using WebView).
let webViewStep = ORKWebViewStep(
identifier: "onboarding.signatureCapture",
html: informedConsentHTML
)
webViewStep.showSignatureAfterContent = true
// The Request Permissions step.
let healthKitTypesToWrite: Set<HKSampleType> = [
HKObjectType.quantityType(forIdentifier: .bodyMassIndex)!,
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKObjectType.workoutType()
]
let healthKitTypesToRead: Set<HKObjectType> = [
HKObjectType.characteristicType(forIdentifier: .dateOfBirth)!,
HKObjectType.workoutType(),
HKObjectType.quantityType(forIdentifier: .appleStandTime)!,
HKObjectType.quantityType(forIdentifier: .appleExerciseTime)!
]
let healthKitPermissionType = ORKHealthKitPermissionType(
sampleTypesToWrite: healthKitTypesToWrite,
objectTypesToRead: healthKitTypesToRead
)
let notificationsPermissionType = ORKNotificationPermissionType(
authorizationOptions: [.alert, .badge, .sound]
)
let motionPermissionType = ORKMotionActivityPermissionType()
let requestPermissionsStep = ORKRequestPermissionsStep(
identifier: "onboarding.requestPermissionsStep",
permissionTypes: [
healthKitPermissionType,
notificationsPermissionType,
motionPermissionType
]
)
requestPermissionsStep.title = "Health Data Request"
requestPermissionsStep.text = "Please review the health data types below and enable sharing to contribute to the study."
// Completion Step
let completionStep = ORKCompletionStep(
identifier: "onboarding.completionStep"
)
completionStep.title = "Enrollment Complete"
completionStep.text = "Thank you for enrolling in this study. Your participation will contribute to meaningful research!"
let surveyTask = ORKOrderedTask(
identifier: "onboard",
steps: [
welcomeInstructionStep,
studyOverviewInstructionStep,
webViewStep,
requestPermissionsStep,
completionStep
]
)
return surveyTask
}
// MARK: Check-in Survey
static let checkInIdentifier = "checkin"
static let checkInFormIdentifier = "checkin.form"
static let checkInPainItemIdentifier = "checkin.form.pain"
static let checkInSleepItemIdentifier = "checkin.form.sleep"
static let checkInSleepItemIdentifier2 = "checkin.form.sleep2"
static let TextChoiceQuestionStep = "checkin.form.sleep3"
static func checkInSurvey() -> ORKTask {
let painAnswerFormat = ORKAnswerFormat.scale(
withMaximumValue: 10,
minimumValue: 1,
defaultValue: 0,
step: 1,
vertical: false,
maximumValueDescription: "Very painful",
minimumValueDescription: "No pain"
)
let sleepAnswerFormat = ORKAnswerFormat.scale(
withMaximumValue: 12,
minimumValue: 0,
defaultValue: 0,
step: 1,
vertical: false,
maximumValueDescription: nil,
minimumValueDescription: nil
)
let sleepAnswerFormat2 = ORKAnswerFormat.scale(
withMaximumValue: 12,
minimumValue: 0,
defaultValue: 0,
step: 1,
vertical: false,
maximumValueDescription: nil,
minimumValueDescription: nil
)
let painItem = ORKFormItem(
identifier: checkInPainItemIdentifier,
text: "How would you rate your pain?",
answerFormat: painAnswerFormat
)
painItem.isOptional = false
let sleepItem = ORKFormItem(
identifier: checkInSleepItemIdentifier,
text: "How many hours of sleep did you get last night?",
answerFormat: sleepAnswerFormat
)
sleepItem.isOptional = false
let sleepItem2 = ORKFormItem(
identifier: checkInSleepItemIdentifier2,
text: "How many hours of sleep did you get last night2?",
answerFormat: sleepAnswerFormat2
)
sleepItem2.isOptional = false
let formStep = ORKFormStep(
identifier: checkInFormIdentifier,
title: "Check In",
text: "Please answer the following questions."
)
formStep.formItems = [painItem, sleepItem, sleepItem2]
formStep.isOptional = false
let surveyTask = ORKOrderedTask(
identifier: checkInIdentifier,
steps: [formStep]
)
return surveyTask
}
static func extractAnswersFromCheckInSurvey(
_ result: ORKTaskResult) -> [OCKOutcomeValue]? {
guard
let response = result.results?
.compactMap({ $0 as? ORKStepResult })
.first(where: { $0.identifier == checkInFormIdentifier }),
let scaleResults = response
.results?.compactMap({ $0 as? ORKScaleQuestionResult }),
let painAnswer = scaleResults
.first(where: { $0.identifier == checkInPainItemIdentifier })?
.scaleAnswer,
let sleepAnswer = scaleResults
.first(where: { $0.identifier == checkInSleepItemIdentifier })?
.scaleAnswer
else {
assertionFailure("Failed to extract answers from check in survey!")
return nil
}
var painValue = OCKOutcomeValue(Double(truncating: painAnswer))
painValue.kind = checkInPainItemIdentifier
var sleepValue = OCKOutcomeValue(Double(truncating: sleepAnswer))
sleepValue.kind = checkInSleepItemIdentifier
return [painValue, sleepValue]
}
// MARK: Choice Survey
static let choiceIdentifier = "choice"
static let choiceFormIdentifier = "choice.form"
static let ChoiceQuestion1Step = "checkin.form.choicequestion1"
static let ChoiceQuestionStep = "checkin.form.Choice"
static func Choices() -> ORKTask {
let Choices = [
ORKTextChoice(text: "None", value: 0 as NSNumber),
ORKTextChoice(text: "Slight", value: 1 as NSNumber),
ORKTextChoice(text: "Mild", value: 2 as NSNumber),
ORKTextChoice(text: "Severe", value: 3 as NSNumber),
]
let choiceAnswerFormat = ORKAnswerFormat.choiceAnswerFormat(
with: .singleChoice,
textChoices: Choices
)
let choiceStep = ORKFormItem(
identifier: ChoiceQuestion1Step,
text: "Text question goes here",
answerFormat: choiceAnswerFormat
)
choiceStep.isOptional = false
let formStep = ORKFormStep(
identifier: choiceFormIdentifier,
title: "Choice Question",
text: "Instructions for questions"
)
formStep.formItems = [choiceStep]
formStep.isOptional = false
let choiceTask = ORKOrderedTask(
identifier: choiceIdentifier,
steps: [formStep]
)
return choiceTask
}
static func extractAnswersFromChoiceSurvey(
_ result: ORKTaskResult) -> [OCKOutcomeValue]? {
guard
let choiceResults = result.results?
.compactMap({ $0 as? ORKStepResult })
.first(where: { $0.identifier == choiceFormIdentifier }),
let scale2Results = choiceResults
.results?.compactMap({ $0 as? ORKChoiceQuestionResult }),
//old
/* let choiceAnswer = scale2Results
.first(where: { $0.identifier == ChoiceQuestion1Step })?
.choiceAnswers
*/
let choiceAnswer = scale2Results
.first(where: { $0.identifier == ChoiceQuestion1Step })?
.choiceAnswers as? [ORKTextChoice]
else {
assertionFailure("Failed to extract answers from check in survey!")
return nil
}
//old
/* var choiceValue = OCKOutcomeValue(NSNumber(pointer: choiceAnswer) as! OCKOutcomeValueUnderlyingType)
choiceValue.kind = ChoiceQuestion1Step
*/
let choiceValue = choiceAnswer
// Convert the choice values to `NSNumber`s
.map { $0.value as! NSNumber }
// Convert the choice values to CareKit outcome values
.map { OCKOutcomeValue(Double(truncating: $0)) }
var choiceValue2 = OCKOutcomeValue(NSNumber(pointer: choiceValue) as! OCKOutcomeValueUnderlyingType)
choiceValue2.kind = ChoiceQuestion1Step
return [choiceValue2]
}
// MARK: Range of Motion.
static func rangeOfMotionCheck() -> ORKTask {
let rangeOfMotionOrderedTask = ORKOrderedTask.kneeRangeOfMotionTask(
withIdentifier: "rangeOfMotionTask",
limbOption: .left,
intendedUseDescription: nil,
options: [.excludeConclusion]
)
let completionStep = ORKCompletionStep(identifier: "rom.completion")
completionStep.title = "All done!"
completionStep.detailText = "We know the road to recovery can be tough. Keep up the good work!"
rangeOfMotionOrderedTask.appendSteps([completionStep])
return rangeOfMotionOrderedTask
}
static func extractRangeOfMotionOutcome(
_ result: ORKTaskResult) -> [OCKOutcomeValue]? {
guard let motionResult = result.results?
.compactMap({ $0 as? ORKStepResult })
.compactMap({ $0.results })
.flatMap({ $0 })
.compactMap({ $0 as? ORKRangeOfMotionResult })
.first else {
assertionFailure("Failed to parse range of motion result")
return nil
}
var range = OCKOutcomeValue(motionResult.range)
range.kind = #keyPath(ORKRangeOfMotionResult.range)
return [range]
}
// MARK: 3D Knee Model
static func kneeModel() -> ORKTask {
let instructionStep = ORKInstructionStep(
identifier: "insights.instructionStep"
)
instructionStep.title = "Your Injury Visualized"
instructionStep.detailText = "A 3D model will be presented to give you better insights on your specific injury."
instructionStep.iconImage = UIImage(systemName: "bandage")
let modelManager = ORKUSDZModelManager(usdzFileName: "toy_robot_vintage")
let kneeModelStep = ORK3DModelStep(
identifier: "insights.kneeModel",
modelManager: modelManager
)
let kneeModelTask = ORKOrderedTask(
identifier: "insights",
steps: [instructionStep, kneeModelStep]
)
return kneeModelTask
}
}
No worries at all! All of this info is super helpful and helps me run the issue locally to help us solve it. It looks like I gave you the wrong info earlier, apologies!
Turns out the result object contains a list of NSNumber
s rather than a list of ORKTextChoice
s. Try this out and let me know if it works for you:
static func extractAnswersFromChoiceSurvey(
_ result: ORKTaskResult) -> [OCKOutcomeValue]?
{
guard
let choiceResult = result
.results?
.compactMap({ $0 as? ORKStepResult })
.first(where: { $0.identifier == choiceFormIdentifier }),
let scale2Results = choiceResult
.results?
.compactMap({ $0 as? ORKChoiceQuestionResult }),
let selectedChoices = scale2Results
.first(where: { $0.identifier == ChoiceQuestion1Step })?
// This line was the key!
.choiceAnswers as? [NSNumber]
else {
assertionFailure("Failed to extract answers from check in survey!")
return nil
}
// Convert the choice values to CareKit outcome values
let outcomeValues = selectedChoices
.map { OCKOutcomeValue(Double(truncating: $0)) }
return outcomeValues
}
Hi Gavi!
I appreciate the encouragement - I'm almost there! The code is working well too :) I tried Print
to see the selection value in the debugger terminal ("3.0" on the bottom right) and it's showing - Thank you!!
The last piece is using kind
in order to add the value show the selection on the CareCard (like in the WWDC):
// Convert the choice values to CareKit outcome values
let outcomeValues = selectedChoices
.map { OCKOutcomeValue(Double(truncating: $0)) }
outcomeValues.kind = ChoiceQuestion1Step <---this line ###
// var choiceValue = OCKOutcomeValue(Double(truncating: outcomeValues))
// choiceValue.kind = ChoiceQuestionStep`
It keeps throwing an error regardless of how I try doing in both the Survey.swift and CareFeedViewController.swft. One such error (which I think is the main culprit, I can't figure out how to use kind
in this context with ORKTextChoice
). For example, I get errors like this: Value of type '[OCKOutcomeValue]' has no member 'kind'
.
Is there a way to to use the kind
with your extractAnswersFromChoiceSurvey
code from above? I tried adding kind
in different ways/places (indicated with the //
, like // outcomeValues.kind = ChoiceQuestion1Step
on line 352 above) where I thought it may work based on the WW.
Thank you!!!
PS - Below is Surveys.swift & CareFeedViewController.swift
For reference, this is the Surveys.swift file now:
import CareKitStore
import ResearchKit
struct Surveys {
private init() {}
// MARK: Onboarding
static func onboardingSurvey() -> ORKTask {
// The Welcome Instruction step.
let welcomeInstructionStep = ORKInstructionStep(
identifier: "onboarding.welcome"
)
welcomeInstructionStep.title = "Welcome!"
welcomeInstructionStep.detailText = "Thank you for joining our study. Tap Next to learn more before signing up."
welcomeInstructionStep.image = UIImage(named: "welcome-image")
welcomeInstructionStep.imageContentMode = .scaleAspectFill
// The Informed Consent Instruction step.
let studyOverviewInstructionStep = ORKInstructionStep(
identifier: "onboarding.overview"
)
studyOverviewInstructionStep.title = "Before You Join"
studyOverviewInstructionStep.iconImage = UIImage(systemName: "checkmark.seal.fill")
let heartBodyItem = ORKBodyItem(
text: "The study will ask you to share some of your health data.",
detailText: nil,
image: UIImage(systemName: "heart.fill"),
learnMoreItem: nil,
bodyItemStyle: .image
)
let completeTasksBodyItem = ORKBodyItem(
text: "You will be asked to complete various tasks over the duration of the study.",
detailText: nil,
image: UIImage(systemName: "checkmark.circle.fill"),
learnMoreItem: nil,
bodyItemStyle: .image
)
let signatureBodyItem = ORKBodyItem(
text: "Before joining, we will ask you to sign an informed consent document.",
detailText: nil,
image: UIImage(systemName: "signature"),
learnMoreItem: nil,
bodyItemStyle: .image
)
let secureDataBodyItem = ORKBodyItem(
text: "Your data is kept private and secure.",
detailText: nil,
image: UIImage(systemName: "lock.fill"),
learnMoreItem: nil,
bodyItemStyle: .image
)
studyOverviewInstructionStep.bodyItems = [
heartBodyItem,
completeTasksBodyItem,
signatureBodyItem,
secureDataBodyItem
]
// The Signature step (using WebView).
let webViewStep = ORKWebViewStep(
identifier: "onboarding.signatureCapture",
html: informedConsentHTML
)
webViewStep.showSignatureAfterContent = true
// The Request Permissions step.
let healthKitTypesToWrite: Set<HKSampleType> = [
HKObjectType.quantityType(forIdentifier: .bodyMassIndex)!,
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKObjectType.workoutType()
]
let healthKitTypesToRead: Set<HKObjectType> = [
HKObjectType.characteristicType(forIdentifier: .dateOfBirth)!,
HKObjectType.workoutType(),
HKObjectType.quantityType(forIdentifier: .appleStandTime)!,
HKObjectType.quantityType(forIdentifier: .appleExerciseTime)!
]
let healthKitPermissionType = ORKHealthKitPermissionType(
sampleTypesToWrite: healthKitTypesToWrite,
objectTypesToRead: healthKitTypesToRead
)
let notificationsPermissionType = ORKNotificationPermissionType(
authorizationOptions: [.alert, .badge, .sound]
)
let motionPermissionType = ORKMotionActivityPermissionType()
let requestPermissionsStep = ORKRequestPermissionsStep(
identifier: "onboarding.requestPermissionsStep",
permissionTypes: [
healthKitPermissionType,
notificationsPermissionType,
motionPermissionType
]
)
requestPermissionsStep.title = "Health Data Request"
requestPermissionsStep.text = "Please review the health data types below and enable sharing to contribute to the study."
// Completion Step
let completionStep = ORKCompletionStep(
identifier: "onboarding.completionStep"
)
completionStep.title = "Enrollment Complete"
completionStep.text = "Thank you for enrolling in this study. Your participation will contribute to meaningful research!"
let surveyTask = ORKOrderedTask(
identifier: "onboard",
steps: [
welcomeInstructionStep,
studyOverviewInstructionStep,
webViewStep,
requestPermissionsStep,
completionStep
]
)
return surveyTask
}
// MARK: Check-in Survey
static let checkInIdentifier = "checkin"
static let checkInFormIdentifier = "checkin.form"
static let checkInPainItemIdentifier = "checkin.form.pain"
static let checkInSleepItemIdentifier = "checkin.form.sleep"
static let checkInSleepItemIdentifier2 = "checkin.form.sleep2"
static let TextChoiceQuestionStep = "checkin.form.sleep3"
static func checkInSurvey() -> ORKTask {
let painAnswerFormat = ORKAnswerFormat.scale(
withMaximumValue: 10,
minimumValue: 1,
defaultValue: 0,
step: 1,
vertical: false,
maximumValueDescription: "Very painful",
minimumValueDescription: "No pain"
)
let sleepAnswerFormat = ORKAnswerFormat.scale(
withMaximumValue: 12,
minimumValue: 0,
defaultValue: 0,
step: 1,
vertical: false,
maximumValueDescription: nil,
minimumValueDescription: nil
)
let sleepAnswerFormat2 = ORKAnswerFormat.scale(
withMaximumValue: 12,
minimumValue: 0,
defaultValue: 0,
step: 1,
vertical: false,
maximumValueDescription: nil,
minimumValueDescription: nil
)
let painItem = ORKFormItem(
identifier: checkInPainItemIdentifier,
text: "How would you rate your pain?",
answerFormat: painAnswerFormat
)
painItem.isOptional = false
let sleepItem = ORKFormItem(
identifier: checkInSleepItemIdentifier,
text: "How many hours of sleep did you get last night?",
answerFormat: sleepAnswerFormat
)
sleepItem.isOptional = false
let sleepItem2 = ORKFormItem(
identifier: checkInSleepItemIdentifier2,
text: "How many hours of sleep did you get last night2?",
answerFormat: sleepAnswerFormat2
)
sleepItem2.isOptional = false
let formStep = ORKFormStep(
identifier: checkInFormIdentifier,
title: "Check In",
text: "Please answer the following questions."
)
formStep.formItems = [painItem, sleepItem, sleepItem2]
formStep.isOptional = false
let surveyTask = ORKOrderedTask(
identifier: checkInIdentifier,
steps: [formStep]
)
return surveyTask
}
static func extractAnswersFromCheckInSurvey(
_ result: ORKTaskResult) -> [OCKOutcomeValue]? {
guard
let response = result.results?
.compactMap({ $0 as? ORKStepResult })
.first(where: { $0.identifier == checkInFormIdentifier }),
let scaleResults = response
.results?.compactMap({ $0 as? ORKScaleQuestionResult }),
let painAnswer = scaleResults
.first(where: { $0.identifier == checkInPainItemIdentifier })?
.scaleAnswer,
let sleepAnswer = scaleResults
.first(where: { $0.identifier == checkInSleepItemIdentifier })?
.scaleAnswer
else {
assertionFailure("Failed to extract answers from check in survey!")
return nil
}
var painValue = OCKOutcomeValue(Double(truncating: painAnswer))
painValue.kind = checkInPainItemIdentifier
var sleepValue = OCKOutcomeValue(Double(truncating: sleepAnswer))
sleepValue.kind = checkInSleepItemIdentifier
return [painValue, sleepValue]
}
// MARK: Choice Survey
static let choiceIdentifier = "choice"
static let choiceFormIdentifier = "choice.form"
static let ChoiceQuestion1Step = "checkin.form.choicequestion1"
static let ChoiceQuestionStep = "checkin.form.Choice"
static func Choices() -> ORKTask {
let Choices = [
ORKTextChoice(text: "None", value: 0 as NSNumber),
ORKTextChoice(text: "Slight", value: 1 as NSNumber),
ORKTextChoice(text: "Mild", value: 2 as NSNumber),
ORKTextChoice(text: "Severe", value: 3 as NSNumber),
]
let choiceAnswerFormat = ORKAnswerFormat.choiceAnswerFormat(
with: .singleChoice,
textChoices: Choices
)
let choiceStep = ORKFormItem(
identifier: ChoiceQuestion1Step,
text: "Text question goes here",
answerFormat: choiceAnswerFormat
)
choiceStep.isOptional = false
let formStep = ORKFormStep(
identifier: choiceFormIdentifier,
title: "Choice Question",
text: "Instructions for questions"
)
formStep.formItems = [choiceStep]
formStep.isOptional = false
let choiceTask = ORKOrderedTask(
identifier: choiceIdentifier,
steps: [formStep]
)
return choiceTask
}
static func extractAnswersFromChoiceSurvey(
_ result: ORKTaskResult) -> [OCKOutcomeValue]? {
guard
let choiceResults = result.results?
.compactMap({ $0 as? ORKStepResult })
.first(where: { $0.identifier == choiceFormIdentifier }),
let scale2Results = choiceResults
.results?
.compactMap({ $0 as? ORKChoiceQuestionResult }),
let selectedChoices = scale2Results
.first(where: { $0.identifier == ChoiceQuestion1Step })?
// This line was the key!
.choiceAnswers as? [NSNumber]
else {
assertionFailure("Failed to extract answers from check in survey!")
return nil
}
// Convert the choice values to CareKit outcome values
let outcomeValues = selectedChoices
.map { OCKOutcomeValue(Double(truncating: $0)) }
// outcomeValues.kind = ChoiceQuestion1Step
// var choiceValue = OCKOutcomeValue(Double(truncating: outcomeValues))
// choiceValue.kind = ChoiceQuestionStep
print(outcomeValues)
return outcomeValues
}
// MARK: Range of Motion.
static func rangeOfMotionCheck() -> ORKTask {
let rangeOfMotionOrderedTask = ORKOrderedTask.kneeRangeOfMotionTask(
withIdentifier: "rangeOfMotionTask",
limbOption: .left,
intendedUseDescription: nil,
options: [.excludeConclusion]
)
let completionStep = ORKCompletionStep(identifier: "rom.completion")
completionStep.title = "All done!"
completionStep.detailText = "We know the road to recovery can be tough. Keep up the good work!"
rangeOfMotionOrderedTask.appendSteps([completionStep])
return rangeOfMotionOrderedTask
}
static func extractRangeOfMotionOutcome(
_ result: ORKTaskResult) -> [OCKOutcomeValue]? {
guard let motionResult = result.results?
.compactMap({ $0 as? ORKStepResult })
.compactMap({ $0.results })
.flatMap({ $0 })
.compactMap({ $0 as? ORKRangeOfMotionResult })
.first else {
assertionFailure("Failed to parse range of motion result")
return nil
}
var range = OCKOutcomeValue(motionResult.range)
range.kind = #keyPath(ORKRangeOfMotionResult.range)
return [range]
}
// MARK: 3D Knee Model
static func kneeModel() -> ORKTask {
let instructionStep = ORKInstructionStep(
identifier: "insights.instructionStep"
)
instructionStep.title = "Your Injury Visualized"
instructionStep.detailText = "A 3D model will be presented to give you better insights on your specific injury."
instructionStep.iconImage = UIImage(systemName: "bandage")
let modelManager = ORKUSDZModelManager(usdzFileName: "toy_robot_vintage")
let kneeModelStep = ORK3DModelStep(
identifier: "insights.kneeModel",
modelManager: modelManager
)
let kneeModelTask = ORKOrderedTask(
identifier: "insights",
steps: [instructionStep, kneeModelStep]
)
return kneeModelTask
}
}
From the CareFeedViewController.swift
import CareKit
import CareKitStore
import CareKitUI
import ResearchKit
import UIKit
import os.log
final class CareFeedViewController: OCKDailyPageViewController,
OCKSurveyTaskViewControllerDelegate {
override func dailyPageViewController(
_ dailyPageViewController: OCKDailyPageViewController,
prepare listViewController: OCKListViewController,
for date: Date) {
checkIfOnboardingIsComplete { isOnboarded in
guard isOnboarded else {
let onboardCard = OCKSurveyTaskViewController(
taskID: TaskIDs.onboarding,
eventQuery: OCKEventQuery(for: date),
storeManager: self.storeManager,
survey: Surveys.onboardingSurvey(),
extractOutcome: { _ in [OCKOutcomeValue(Date())] }
)
onboardCard.surveyDelegate = self
listViewController.appendViewController(
onboardCard,
animated: false
)
return
}
let isFuture = Calendar.current.compare(
date,
to: Date(),
toGranularity: .day) == .orderedDescending
self.fetchTasks(on: date) { tasks in
tasks.compactMap {
let card = self.taskViewController(for: $0, on: date)
card?.view.isUserInteractionEnabled = !isFuture
card?.view.alpha = isFuture ? 0.4 : 1.0
return card
}.forEach {
listViewController.appendViewController($0, animated: false)
}
}
}
}
private func checkIfOnboardingIsComplete(_ completion: @escaping (Bool) -> Void) {
var query = OCKOutcomeQuery()
query.taskIDs = [TaskIDs.onboarding]
storeManager.store.fetchAnyOutcomes(
query: query,
callbackQueue: .main) { result in
switch result {
case .failure:
Logger.feed.error("Failed to fetch onboarding outcomes!")
completion(false)
case let .success(outcomes):
completion(!outcomes.isEmpty)
}
}
}
private func fetchTasks(
on date: Date,
completion: @escaping([OCKAnyTask]) -> Void) {
var query = OCKTaskQuery(for: date)
query.excludesTasksWithNoEvents = true
storeManager.store.fetchAnyTasks(
query: query,
callbackQueue: .main) { result in
switch result {
case .failure:
Logger.feed.error("Failed to fetch tasks for date \(date)")
completion([])
case let .success(tasks):
completion(tasks)
}
}
}
private func taskViewController(
for task: OCKAnyTask,
on date: Date) -> UIViewController? {
switch task.id {
case TaskIDs.checkIn:
let survey = OCKSurveyTaskViewController(
task: task,
eventQuery: OCKEventQuery(for: date),
storeManager: storeManager,
survey: Surveys.checkInSurvey(),
viewSynchronizer: SurveyViewSynchronizer(),
extractOutcome: Surveys.extractAnswersFromCheckInSurvey
)
survey.surveyDelegate = self
return survey
case TaskIDs.choice:
let survey = OCKSurveyTaskViewController(
task: task,
eventQuery: OCKEventQuery(for: date),
storeManager: storeManager,
survey: Surveys.Choices(),
viewSynchronizer: SurveyViewSynchronizer(),
extractOutcome: Surveys.extractAnswersFromChoiceSurvey
)
survey.surveyDelegate = self
return survey
case TaskIDs.rangeOfMotionCheck:
let survey = OCKSurveyTaskViewController(
task: task,
eventQuery: OCKEventQuery(for: date),
storeManager: storeManager,
survey: Surveys.rangeOfMotionCheck(),
extractOutcome: Surveys.extractRangeOfMotionOutcome
)
survey.surveyDelegate = self
return survey
default:
return nil
}
}
func checkIfChoiceIsComplete(_ completion: @escaping (Bool) -> Void) {
var query = OCKOutcomeQuery()
query.taskIDs = [TaskIDs.choice]
storeManager.store.fetchAnyOutcomes(
query: query,
callbackQueue: .main) { result in
switch result {
case .failure:
Logger.feed.error("Failed to fetch onboarding outcomes!")
completion(false)
case let .success(outcomes):
completion(!outcomes.isEmpty)
}
}
}
// MARK: SurveyTaskViewControllerDelegate
func surveyTask(
viewController: OCKSurveyTaskViewController,
for task: OCKAnyTask,
didFinish result: Result<ORKTaskViewControllerFinishReason, Error>) {
if case let .success(reason) = result, reason == .completed {
reload()
}
}
func surveyTask(
viewController: OCKSurveyTaskViewController,
shouldAllowDeletingOutcomeForEvent event: OCKAnyEvent) -> Bool {
event.scheduleEvent.start >= Calendar.current.startOfDay(for: Date())
}
}
final class SurveyViewSynchronizer: OCKSurveyTaskViewSynchronizer {
override func updateView(
_ view: OCKInstructionsTaskView,
context: OCKSynchronizationContext<OCKTaskEvents>) {
super.updateView(view, context: context)
if let event = context.viewModel.first?.first, event.outcome != nil {
view.instructionsLabel.isHidden = false
let pain = event.answer(kind: Surveys.checkInPainItemIdentifier)
let sleep = event.answer(kind: Surveys.checkInSleepItemIdentifier)
let choice = event.answer(kind: Surveys.ChoiceQuestion1Step)
view.instructionsLabel.text = """
Pain: \(Int(pain))
Sleep: \(Int(sleep)) hours
Choice: \(Int(choice))
"""
} else {
view.instructionsLabel.isHidden = true
}
}
}
The error you're seeing here is happening because outcomeValues
is an array of outcome values, but the kind
property is only available on each individual outcome value. We can modify the snippet a bit and set the kind
when we create the array of outcome values:
static func extractAnswersFromChoiceSurvey(
_ result: ORKTaskResult) -> [OCKOutcomeValue]?
{
guard
let choiceResult = result
.results?
.compactMap({ $0 as? ORKStepResult })
.first(where: { $0.identifier == choiceFormIdentifier }),
let scale2Results = choiceResult
.results?
.compactMap({ $0 as? ORKChoiceQuestionResult }),
let selectedChoices = scale2Results
.first(where: { $0.identifier == ChoiceQuestion1Step })?
.choiceAnswers as? [NSNumber]
else {
assertionFailure("Failed to extract answers from check in survey!")
return nil
}
// Convert the choice values to CareKit outcome values
let outcomeValues = selectedChoices.map { selectedChoice -> OCKOutcomeValue in
var outcomeValue = OCKOutcomeValue(Double(truncating: selectedChoice))
// Set the kind here!
outcomeValue.kind = ChoiceQuestionStep
return outcomeValue
}
return outcomeValues
}
Hi Gavi!
It works!! Thank you so much for all of your help - you've been great!! :)
Glad to hear it!
Good day!
I have been following the WWDC 21 hosted by @erik-apple and it's been really great! I had a question and wasn't sure where else would be a good place as I believe it is a CareKit issue (not 100% and still fairly new here). I see that Erik helps out on here and was hoping he, or someone, could help with something pertaining to CareKit in Part 2 of the WWDC.
I have successfully created two ResearchKit ORKTextChoice questions (.singleChoice ) and I'm trying to pass them to CareKit like Erik does with the ORKScaleQuestionResult (.scaleAnswer). Example from my code:
I was able to modify Erik's code from WWDC to extract the answers:
Note: After running the app and submitting the ORKTextChoice answer responses in the simulator, when I go back over to Xcode I can see the correct answer choice when I hover over both "choiceAnswer" and "choiceAnswer2". I take as meaning the code works for extracting the answer choices. This can be seen in the output terminal as well:
Printing description of choiceAnswer: ([NSCoding & NSCopying & NSObject]) choiceAnswer = 1 value { [0] = 0x99d102272eb2af4b Int64(3) } Printing description of ((__NSCFNumber *)0x99d102272eb2af4b): 3
...where "3" at the very end is the value from the ORKTextChoice "choiceAnswer".
In Erik's tutorial the next step is to pass the ResearchKit ORKScaleQuestionResult to a CareKit OCKOutcomeValue using the following code:
I have tried for days to pass the ResearchKit ORKTextChoice to CareKit and Xcode corrected the code to what you see below but still crashes with
Thread 1: signal SIGABRT
on the following line of code:var Value2 = OCKOutcomeValue(NSNumber(pointer: choiceAnswer) as! OCKOutcomeValueUnderlyingType) Value.kind = Question1Step
I have a feeling that the error has something to do with this part of the above code:
OCKOutcomeValue(NSNumber(pointer:
and/oras! OCKOutcomeValueUnderlyingType)
as I get the terminal error:Could not cast value of type 'NSConcreteValue' (0x15e0dcf38) to 'CareKitStore.OCKOutcomeValueUnderlyingType' (0x15e0d9ad0)
As mentioned I have been searching for days and haven't found anything helpful. I am hoping that someone may have some guidance and I would be very grateful! :)
Thank you!
PS - I was trying to be concise, so if more explanation or code is needed, please let me know!