CareKit icon indicating copy to clipboard operation
CareKit copied to clipboard

How to store multiple survey steps in appendOutcomeValue in CareKit

Open umair-shams opened this issue 5 years ago • 9 comments

Hello Developers. Here I am facing issue regarding the storing of two survey steps in appendOutcomeValue. Actually I have two steps but When i fetch events using OCKEventAggregator it returns one value. I am storing the results with code given below.

         let answerFormat = ORKAnswerFormat.scale(withMaximumValue: 10, minimumValue: 1, defaultValue: 5, step: 1, 
       vertical: 
        false,
                                             maximumValueDescription: "Very painful", minimumValueDescription: "No pain")
        let painStep = ORKQuestionStep(identifier: "headPain", title: "Pain Survey", question: "Rate your pain", answer: 
        answerFormat)
         let surveyTask = ORKOrderedTask(identifier: "survey", steps: [painStep])
         let surveyViewController = ORKTaskViewController(task: surveyTask, taskRun: nil)
         surveyViewController.delegate = self
          present(surveyViewController, animated: true, completion: nil)
    

    let painStep1 = ORKQuestionStep(identifier: "LegPain", title: "Pain Survey", question: "Rate your pain", answer: 
    answerFormat)
    let surveyTask1 = ORKOrderedTask(identifier: "survey", steps: [painStep1])
    let surveyViewControlle1r = ORKTaskViewController(task: surveyTask1, taskRun: nil)
    surveyViewController1.delegate = self
    present(surveyViewController1, animated: true, completion: nil)

    let survey = taskViewController.result.results!.first(where: { $0.identifier == "headPain" }) as! ORKStepResult
    let painResult = survey.results!.first as! ORKScaleQuestionResult
    let answer = Int(truncating: painResult.scaleAnswer!)

    let survey1 = taskViewController.result.results!.first(where: { $0.identifier == "LegPain" }) as! ORKStepResult
    let painResult1 = survey1.results!.first as! ORKScaleQuestionResult
    let answer1 = Int(truncating: painResult1.scaleAnswer!)

    controller.appendOutcomeValue(withType: answer, at: IndexPath(item: 0, section: 0), completion: nil) // It only return this
    controller.appendOutcomeValue(withType: answer1, at: IndexPath(item: 0, section: 0), completion: nil) // not fetching 

And I using OCKEventAggregator to fetch events:

  let myCustomAggregator = OCKEventAggregator.custom { dailyEvents -> Double in
                let values = dailyEvents.map { $0.outcome?.values.first?.integerValue ?? 0 }
                print(values) // It return only one value
                let sumTotal = values.reduce(0, +)
                
                return Double(sumTotal)
            }

umair-shams avatar Jul 09 '20 15:07 umair-shams

Hi Umair,

Does changing your code to this fix the problem?

let myCustomAggregator = OCKEventAggregator.custom { dailyEvents -> Double in
   
    // There are two values, but you were using `first`, which only gets the first of the two.
    let values: [Int] = dailyEvents.flatMap { $0.outcome?.values.map { $0.integerValue ?? 0 } ?? [] }

    print(values) // Should print array of two values now

    let sumTotal = values.reduce(0, +)
    return Double(sumTotal)
}

erik-apple avatar Jul 14 '20 16:07 erik-apple

Hi @erik-apple

I have used the approach you've recommended, but the system is still ignoring the second value.

In the survey, I have a form with two items and I've verified in debug mode that the variables "answer" and "answer2" are getting populated with the values I input in the frontend:

[...]

        // 4a. Retrieve the result from the ResearchKit survey
        
        let survey = taskViewController.result.results!.first(where: { $0.identifier == "surveyform" }) as! ORKStepResult
        let painResult = survey.results!.first(where: { $0.identifier == "pain" }) as! ORKScaleQuestionResult
        let answer = Int(truncating: painResult.scaleAnswer!)

        let survey2 = taskViewController.result.results!.first(where: { $0.identifier == "surveyform" }) as! ORKStepResult
        let pain2Result = survey2.results!.first(where: { $0.identifier == "pain2" })  as! ORKScaleQuestionResult
        let answer2 = Int(truncating: pain2Result.scaleAnswer!)

        // 4b. Save the result into CareKit's store
        controller.appendOutcomeValue(withType: answer, at: IndexPath(item: 0, section: 0), completion: nil)
        controller.appendOutcomeValue(withType: answer2, at: IndexPath(item: 0, section: 0), completion: nil)

Here the code I use for the custom aggregator:

[...]
                    let myCustomAggregator = OCKEventAggregator.custom { dailyEvents -> Double in

                        // This closure will be called once for each day on the chart.
                        // `dailyEvents` is an array containing the events for one day.
                        //
                        // As the developer, you must convert that array into a `Double`
                        // to be used for Y-Axis value on the chart. This might require
                        // knowledge about how many events or values are expected.

                        // If there is no outcome, we will count that as zero. We sum all the values and use that result.
                        
                        let values2: [Int] = dailyEvents.flatMap { $0.outcome?.values.map { $0.integerValue ?? 0 } ?? [] }
                        
                        print(values2) // Should print array of two values
                        
                        let sumTotal = values2.reduce(0, +)
                        
                        return Double(sumTotal)
                    }
                    
                    let painDataSeries = OCKDataSeriesConfiguration(
                        taskID: "survey",
                        legendTitle: "Pain",
                        gradientStartColor: UIColor.FlatColor.Red.kDarkRed, 
                        gradientEndColor: UIColor.FlatColor.Red.kDarkRed,
                        markerSize: 10,
                        eventAggregator: myCustomAggregator)

When I select the answers as follows... image

...only the first value (3) gets considered by the system (see results of "print(values2)"):

image

Am I missing something?

Thanks in advance, Andrea

andreaxricci avatar Dec 23 '20 15:12 andreaxricci

Andrea,

I suspect the issue is here:

// 4b. Save the result into CareKit's store
controller.appendOutcomeValue(withType: answer, at: IndexPath(item: 0, section: 0), completion: nil)
controller.appendOutcomeValue(withType: answer2, at: IndexPath(item: 0, section: 0), completion: nil)

You might be hitting an error on the 2nd line, but it's tough to tell since the completion closure, where the error would be passed back to you, is set to nil.

The append method you're using is convenient when adding a single outcome value, but it might not behave the way you expect if you call it twice in a row without waiting for the first call to complete before starting the second.

It's a bit more verbose, but the proper way to handle this would be to attach all the outcome values in a single save operation.

// 4b. Attach the outcome values to an outcome and add the outcome to the event.
// (This assumes that no outcome exists already)
if let event = controller.eventFor(indexPath: eventIndexPath) {

    let values = [1, 2, 3].map { OCKOutcomeValue($0) } // Replace 1, 2, 3 with the values from your survey

    let outcome = try! controller.makeOutcomeFor(event: event, withValues: values)

    controller.storeManager.store.addAnyOutcome(outcome, callbackQueue: .main) { result in

        // I recommend not leaving the completion closure nil.
        // Inspecting the result can alert you to problems in your app.
        switch result {

        case let .success(updatedOutcome):
            print("Success: \(updatedOutcome.values)")

        case let .failure(error):
            print("Error: \(error)")
        }
    }
}

erik-apple avatar Dec 23 '20 23:12 erik-apple

many thanks @erik-apple, this helped!

andreaxricci avatar Dec 24 '20 04:12 andreaxricci

Hi @erik-apple, thanks for 4b. above That works well. How should we write the custom aggregator now. The way that it is mentioned above does not seem to print array of two values now. How do we get both values to show on a graph, then?

scdi avatar Feb 22 '21 05:02 scdi

Hi @erik-apple, the code by @UmairShams does work in combination with the code you provided in 4b. I am able to get an array of two values.

scdi avatar Feb 22 '21 14:02 scdi

Glad to hear you got it working!

erik-apple avatar Feb 22 '21 15:02 erik-apple

Actually the code is working but I was not able to get the graph to work properly. How would you set-up the graph to show both values?

On Mon, Feb 22, 2021 at 10:53 AM erik-apple [email protected] wrote:

Glad to hear you got it working!

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/carekit-apple/CareKit/issues/473#issuecomment-783473121, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADHK7LYOMXK7EJOHTJGPHL3TAJ4XRANCNFSM4OVWK7FQ .

-- Jude Jonassaint, RN Sicklesoft, Inc

Let whoever is in charge keep this simple question in her head (not, how can I always do this right thing myself, but) how can I provide for this right thing to be always done https://everydaypowerblog.com/quotes-by-martin-luther-king-jr/? – Florence Nightingale

scdi avatar Feb 22 '21 21:02 scdi

Without knowing precisely what effect you're going for, my best guess is that you need two create two different data series configurations, one that looks only at the first value, and one that looks at the second.

Both series will refer to the same taskID, but each will use a different custom event aggregator to extract the value to display on the Y-axis. You'll probably wind up with something that looks something like this.

let answerSeries1 = OCKDataSeriesConfiguration(
    taskID: "survey",
    legendTitle: "Answer 1",
    gradientStartColor: .systemGray2,
    gradientEndColor: .systemGray,
    markerSize: 10,
    eventAggregator: OCKEventAggregator.custom({ events in events.first?.outcome?.values[0].doubleValue ?? 0 })

let answerSeries2 = OCKDataSeriesConfiguration(
    taskID: "survey",
    legendTitle: "Answer 2",
    gradientStartColor: .systemGray2,
    gradientEndColor: .systemGray,
    markerSize: 10,
    eventAggregator: OCKEventAggregator.custom({ events in events.first?.outcome?.values[1].doubleValue ?? 0 })

let insightsCard = OCKCartesianChartViewController(
    plotType: .bar,
    selectedDate: date,
    configurations: [answerSeries1, answerSeries2],
    storeManager: self.storeManager)

erik-apple avatar Feb 22 '21 22:02 erik-apple