carekit-apple / CareKit

CareKit is an open source software framework for creating apps that help people better understand and manage their health.
https://www.researchandcare.org
Other
2.41k stars 444 forks source link

How to Integrate ResearchKit surveys with CareKit 2.0 #325

Closed smundada closed 4 years ago

smundada commented 4 years ago

I'm currently having lots of trouble integrating surveys/assessments from ResearchKit into my Care Card. I was looking through documentation on http://carekit.org/docs/docs/Overview/Overview.html#IOS, but it seems like tasks and data types related to surveys/assessments are outdated and were in the older CareKit version (in the file OCKCarePlanActivity). I'm also don't see much about this issue in the current documentation and online. Would someone be able to help me understand a good way to incorporate surveys/assessments into CareKit 2.0?

erik-apple commented 4 years ago

Thanks for raising an issue! We've gotten a handful of questions about ResearcKit integration. Next week we will provide some sample code for you and look into how we can improve the documentation.

smundada commented 4 years ago

Thank you! That would be super helpful.

smundada commented 4 years ago

Hi! Just wanted to check when you would be able to provide some sample code for this issue? Thank you!

erik-apple commented 4 years ago

Thanks for your patience!

The gist of what you need to do is...

  1. Subclass an existing task view controller
  2. Override the method that is called when the task is completed
  3. Present a ResearchKit survey and wait for the user to complete it
  4. Get the survey result and save it to CareKit's store

Here is an example demonstrating how to prompt the user to rate their pain on a scale of 1-10. Keep in mind as you're reading the code below that CareKit and ResearchKit both use the term "task", but that they are distinct.

// 1. Subclass a task view controller to customize the control flow and present a ResearchKit survey!
class SurveyViewController: OCKInstructionsTaskViewController, ORKTaskViewControllerDelegate {

    // 2. This method is called when the use taps the button!
    override func taskView(_ taskView: UIView & OCKTaskDisplayable, didCompleteEvent isComplete: Bool, at indexPath: IndexPath, sender: Any?) {

        // 2a. If the task was uncompleted, fall back on the super class's default behavior or deleting the outcome.
        if !isComplete {
            super.taskView(taskView, didCompleteEvent: isComplete, at: indexPath, sender: sender)
            return
        }

        // 2b. If the user attemped to mark the task complete, display a ResearchKit survey.
        let answerFormat = ORKAnswerFormat.scale(withMaximumValue: 10, minimumValue: 1, defaultValue: 5, step: 1, vertical: false,
                                                 maximumValueDescription: "Very painful", minimumValueDescription: "No pain")
        let painStep = ORKQuestionStep(identifier: "pain", 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)
    }

    // 3. This method will be called when the user completes the survey. 
    // Extract the result and save it to CareKit's store!
    func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
        taskViewController.dismiss(animated: true, completion: nil)
        guard reason == .completed else {
            taskView.completionButton.isSelected = false
            return
        }

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

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

Once you have defined this view controller, you can add it into your app as you would any other CareKit view controller!

let todaysSurveyCard = SurveyViewController(
    taskID: "survey", 
    eventQuery: OCKEventQuery(for: Date()), 
    storeManager: storeManager)

present(surveyCard, animated: true, completion: nil)

You may also decide that you want the view to update to display the result of your survey instead of the default values used by the superclass. To change that, you can implement your own view synchronizer.

class SurveyViewSynchronizer: OCKInstructionsTaskViewSynchronizer {

    // Customize the initial state of the view
    override func makeView() -> OCKInstructionsTaskView {
        let instructionsView = super.makeView()
        instructionsView.completionButton.label.text = "Start Survey"
        return instructionsView
    }

    // Customize how the view updates
    override func updateView(_ view: OCKInstructionsTaskView, 
                             context: OCKSynchronizationContext<OCKTaskEvents?>) {
        super.updateView(view, context: context)

        // Check if an answer exists or not and set the detail label accordingly
        if let answer = context.viewModel?.firstEvent?.outcome?.values.first?.integerValue {
            view.headerView.detailLabel.text = "Pain Rating: \(answer)"
        } else {
            view.headerView.detailLabel.text = "Rate your pain on a scale of 1 to 10"
        }
    }
}

Now, when you create an instance of your SurveyViewController, you can pass in your custom view synchronizer to change how the view updates.

let surveyCard = SurveyViewController(
    viewSynchronizer: SurveyViewSynchronizer(), 
    taskID: "survey",                                             
    eventQuery: OCKEventQuery(date: Date()), 
    storeManager: storeManager)

present(surveyCard, animated: true, completion: nil)

Hopefully that will get you moving in the right direction. If you have any more questions, please let us know!

erik-apple commented 4 years ago

We've updated the README with this information as well. I'm going to go ahead and close this issue. If you have any more questions, feel free to reopen it though!

miguelc95 commented 4 years ago

Hi Erik! I've done what you mentioned about sublassing OCKInstructionsTaskViewController, but as I am using Swift UI I cannot make it a UIViewController representable since I am "redeclarating" SurveyViewController, do you know of a way of integrating a ResearchKit survey using swift ui?

erik-apple commented 4 years ago

Based on #352, I'm guessing you got this worked out on your own!

andreaxricci commented 3 years ago

sorry if I jump in within this closed post.. I tried to integrate a survey from ResearchKit into my app, following the recommendations above, but I'm experiencing an issue: for a given date, the survey gets properly triggered and once completed, it is marked as such and can't be triggered anymore. All good so far, but when I move to a different date and I navigate back to the initial one, the survey does no longer appear in status completed, but is available again for selection. Did you experience this too? Any idea which step am I missing? (code below)


[...]

      // Add survey card
       let surveyCard = SurveyViewController(
                        viewSynchronizer: SurveyViewSynchronizer(),
                        taskID: "survey",
                        eventQuery: OCKEventQuery(for: Date()),
                        storeManager: self.storeManager)

       listViewController.appendViewController(surveyCard, animated: false)

[...]

// 1. Subclass a task view controller to customize the control flow and present a ResearchKit survey
class SurveyViewController: OCKInstructionsTaskViewController, ORKTaskViewControllerDelegate {

    // 2. This method is called when the use taps the button!
    override func taskView(_ taskView: UIView & OCKTaskDisplayable, didCompleteEvent isComplete: Bool, at indexPath: IndexPath, sender: Any?) {

        // 2a. If the task was uncompleted, fall back on the super class's default behavior or deleting the outcome.
        if !isComplete {
            super.taskView(taskView, didCompleteEvent: isComplete, at: indexPath, sender: sender)
            return
        }

        // 2b. If the user attemped to mark the task complete, display a ResearchKit survey.
        let answerFormat = ORKAnswerFormat.scale(withMaximumValue: 10, minimumValue: 1, defaultValue: 5, step: 1, vertical: false,
                                                 maximumValueDescription: "Very painful", minimumValueDescription: "No pain")
        //let answer2Format = ORKAnswerFormat.scale(withMaximumValue: 10, minimumValue: 1, defaultValue: 5, step: 1, vertical: false,
                                                 //maximumValueDescription: "Very high", minimumValueDescription: "Very low")
        let painStep = ORKQuestionStep(identifier: "pain", title: "Daily Survey", question: "Rate your pain", answer: answerFormat)
        //let pain2Step = ORKQuestionStep(identifier: "frustration", title: "Daily Survey", question: "Rate your level of frustration", answer: answer2Format)
        let surveyTask = ORKOrderedTask(identifier: "survey", steps: [painStep])
        //let surveyTask = ORKOrderedTask(identifier: "survey", steps: [painStep, pain2Step])
        let surveyViewController = ORKTaskViewController(task: surveyTask, taskRun: nil)
        surveyViewController.delegate = self

        present(surveyViewController, animated: true, completion: nil)
    }

    // 3. This method will be called when the user completes the survey.
    // Extract the result and save it to CareKit's store!
    func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
        taskViewController.dismiss(animated: true, completion: nil)
        guard reason == .completed else {
            taskView.completionButton.isSelected = false
            return
        }

        // 4a. Retrieve the result from the ResearchKit survey
        let survey = taskViewController.result.results!.first(where: { $0.identifier == "pain" }) as! ORKStepResult
        let painResult = survey.results!.first as! ORKScaleQuestionResult
        let answer = Int(truncating: painResult.scaleAnswer!)
        //let survey2 = taskViewController.result.results!.first(where: { $0.identifier == "frustration" }) as! ORKStepResult
        //let pain2Result = survey2.results!.first 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)

    }
}

class SurveyViewSynchronizer: OCKInstructionsTaskViewSynchronizer {

    // Customize the initial state of the view
    override func makeView() -> OCKInstructionsTaskView {
        let instructionsView = super.makeView()
        instructionsView.completionButton.label.text = "Start survey"
        return instructionsView
    }

    // Customize the view updated
    override func updateView(_ view: OCKInstructionsTaskView,
                             context: OCKSynchronizationContext<OCKTaskEvents?>) {
        super.updateView(view, context: context)

        // Check if answer exists or not
        if let answer = context.viewModel?.firstEvent?.outcome?.values.first?.integerValue {
            view.headerView.detailLabel.text = "Pain level: \(answer)"
        } else {
            view.headerView.detailLabel.text = "Take survey"
        }
    }
}
erik-apple commented 3 years ago

@andreaxricci If the code you provided is the actual code you're using, then it's possible problem is the date you're passing to the task view controller.

       // Add survey card
       let surveyCard = SurveyViewController(
                        viewSynchronizer: SurveyViewSynchronizer(),
                        taskID: "survey",
                        eventQuery: OCKEventQuery(for: Date()), // This should probably be `date` instead of `Date()`
                        storeManager: self.storeManager)
andreaxricci commented 3 years ago

@erik-apple thanks for your quick reply. I tried changing that, but the outcome doesn't change. Here is the code I'm using (mainly taken from the tutorials & other comments found in the issue section of CareKit and ResearchKit):


import SwiftUI
import UIKit
import CareKit
import ResearchKit

//Adding the store manager into the App Delegate
class AppDelegate: UIResponder, UIApplicationDelegate {
    // Manages synchronization of a CoreData store
    static var synchronizedStoreManager: OCKSynchronizedStoreManager = {
        let store = OCKStore(name: "SampleAppStore", type: .onDisk)
        store.populateSampleData()
        let manager = OCKSynchronizedStoreManager(wrapping: store)
        return manager
    }()

     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         return true
     }

     func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
         return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
     }
 }

// Extensions for Store Manager
extension OCKSynchronizedStoreManager: ObservableObject {}

// Hardcode some data into the Store, to test the App
private extension OCKStore {

    // Adds tasks and contacts into the store
    func populateSampleData() {

        let thisMorning = Calendar.current.startOfDay(for: Date())
        let aFewDaysAgo = Calendar.current.date(byAdding: .day, value: -4, to: thisMorning)!
        let beforeBreakfast = Calendar.current.date(byAdding: .hour, value: 8, to: aFewDaysAgo)!
        let afterLunch = Calendar.current.date(byAdding: .hour, value: 14, to: aFewDaysAgo)!

        let schedule = OCKSchedule(composing: [
            OCKScheduleElement(start: beforeBreakfast, end: nil,
                               interval: DateComponents(day: 1)),

            OCKScheduleElement(start: afterLunch, end: nil,
                               interval: DateComponents(day: 2))
        ])

        var doxylamine = OCKTask(id: "doxylamine", title: "Take Doxylamine",
                                 carePlanID: nil, schedule: schedule)
        doxylamine.instructions = "Take 25mg of doxylamine when you experience nausea."

        let nauseaSchedule = OCKSchedule(composing: [
            OCKScheduleElement(start: beforeBreakfast, end: nil, interval: DateComponents(day: 1),
                               text: "Anytime throughout the day", targetValues: [], duration: .allDay)
            ])

        var nausea = OCKTask(id: "nausea", title: "Track your nausea",
                             carePlanID: nil, schedule: nauseaSchedule)
        nausea.impactsAdherence = false
        nausea.instructions = "Tap the button below anytime you experience nausea."

        let kegelElement = OCKScheduleElement(start: beforeBreakfast, end: nil, interval: DateComponents(day: 2))
        let kegelSchedule = OCKSchedule(composing: [kegelElement])
        var kegels = OCKTask(id: "kegels", title: "Kegel Exercises", carePlanID: nil, schedule: kegelSchedule)
        kegels.impactsAdherence = true
        kegels.instructions = "Perform kegel exercies"

        addTasks([nausea, doxylamine, kegels], callbackQueue: .main, completion: nil)

        var contact1 = OCKContact(id: "jane", givenName: "Jane",
                                  familyName: "Daniels", carePlanID: nil)
        contact1.asset = "JaneDaniels"
        contact1.title = "Family Practice Doctor"
        contact1.role = "Dr. Daniels is a family practice doctor with 8 years of experience."

        contact1.address = {
            let address = OCKPostalAddress()
            address.street = "2598 Reposa Way"
            address.city = "San Francisco"
            address.state = "CA"
            address.postalCode = "94127"
            return address
        }()

        var contact2 = OCKContact(id: "matthew", givenName: "Matthew",
                                  familyName: "Reiff", carePlanID: nil)
        contact2.asset = "MatthewReiff"
        contact2.title = "OBGYN"
        contact2.role = "Dr. Reiff is an OBGYN with 13 years of experience."
        contact2.address = {
            let address = OCKPostalAddress()
            address.street = "396 El Verano Way"
            address.city = "San Francisco"
            address.state = "CA"
            address.postalCode = "94127"
            return address
        }()

        addContacts([contact1, contact2])

    }
}

class CareViewController: OCKDailyPageViewController {

    // show the list of doctors when clicking on button "Care Team", by calling function presentContactsListViewController()
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.rightBarButtonItem =
            UIBarButtonItem(title: "Care Team", style: .plain, target: self,
                            action: #selector(presentContactsListViewController))
    }
    // definition of function presentContactsListViewController(), used to show the list of doctors. It is closed, when clicking on button "Done", which triggers function dismissContactsListViewController()
    @objc private func presentContactsListViewController() {
        let viewController = OCKContactsListViewController(storeManager: storeManager)
        viewController.title = "Care Team"
        viewController.isModalInPresentation = true
        viewController.navigationItem.rightBarButtonItem =
            UIBarButtonItem(title: "Done", style: .plain, target: self,
                            action: #selector(dismissContactsListViewController))

        let navigationController = UINavigationController(rootViewController: viewController)
        present(navigationController, animated: true, completion: nil)
    }
    // definition of function dismissContactsListViewController(), which closes the list of doctors
    @objc private func dismissContactsListViewController() {
        dismiss(animated: true, completion: nil)
    }

    // This will be called each time the selected date changes.
    // It can be used as an opportunity to rebuild the content shown to the user.
    override func dailyPageViewController(_ dailyPageViewController: OCKDailyPageViewController,
                                          prepare listViewController: OCKListViewController, for date: Date) {
        // Define a query in order to search in the store for tasks related to the identifiers listed below, by date. Empty results are excluded.
        let identifiers = ["doxylamine", "nausea", "kegels", "steps", "heartRate", "survey"]
        var query = OCKTaskQuery(for: date)
        query.ids = identifiers
        query.excludesTasksWithNoEvents = true

        // Apply the query to the store manager
        storeManager.store.fetchAnyTasks(query: query, callbackQueue: .main) { result in
            switch result {
            case .failure(let error): print("Error: \(error)")
            case .success(let tasks):
                // If the query was executed without errors, the system continues to process the results, integrating them to the rest of the components.
                // At first, a non-CareKit view is added into the list, using file TipView.swift
                let tipTitle = "Benefits of exercising"
                let tipText = "Learn how activity can promote a healthy pregnancy"

                // Only show the tip view on the current date
                if Calendar.current.isDate(date, inSameDayAs: Date()) {
                    let tipView = TipView()
                    tipView.headerView.titleLabel.text = tipTitle
                    tipView.headerView.detailLabel.text = tipText
                    tipView.imageView.image = UIImage(named: "exercise.jpeg")
                    listViewController.appendView(tipView, animated: false)
                }

                // Create a card for the kegel task (kegelsCard). Note: since the kegel task is only scheduled every other day, there will be cases where it is not contained in the tasks array returned from the query.
                if let kegelsTask = tasks.first(where: { $0.id == "kegels" }) {
                    let kegelsCard = OCKSimpleTaskViewController(
                        task: kegelsTask,
                        eventQuery: .init(for: date),
                        storeManager: self.storeManager)

                    listViewController.appendViewController(kegelsCard, animated: false)
                }

                // Create a card for the doxylamine task (doxylamineCard) if there are events for it on this day.
                if let doxylamineTask = tasks.first(where: { $0.id == "doxylamine" }) {

                    let doxylamineCard = OCKChecklistTaskViewController(
                        task: doxylamineTask,
                        eventQuery: .init(for: date),
                        storeManager: self.storeManager)

                    listViewController.appendViewController(doxylamineCard, animated: false)
                }

                // Create a card for the nausea task (insightsCard) if there are events for it on this day.
                // Its OCKSchedule was defined to have daily events, so this task should be found in `tasks` every day after the task start date.
                if let nauseaTask = tasks.first(where: { $0.id == "nausea" }) {

                    // dynamic gradient colors to be used in the insights chart
                    let nauseaGradientStart = UIColor { traitCollection -> UIColor in
                        return traitCollection.userInterfaceStyle == .light ? #colorLiteral(red: 0.9960784314, green: 0.3725490196, blue: 0.368627451, alpha: 1) : #colorLiteral(red: 0.8627432641, green: 0.2630574384, blue: 0.2592858295, alpha: 1)
                    }
                    let nauseaGradientEnd = UIColor { traitCollection -> UIColor in
                        return traitCollection.userInterfaceStyle == .light ? #colorLiteral(red: 0.9960784314, green: 0.4732026144, blue: 0.368627451, alpha: 1) : #colorLiteral(red: 0.8627432641, green: 0.3598620686, blue: 0.2592858295, alpha: 1)
                    }

                    // Create a plot comparing nausea to medication adherence.
                    let nauseaDataSeries = OCKDataSeriesConfiguration(
                        taskID: "nausea",
                        legendTitle: "Nausea",
                        gradientStartColor: nauseaGradientStart,
                        gradientEndColor: nauseaGradientEnd,
                        markerSize: 10,
                        eventAggregator: OCKEventAggregator.countOutcomeValues)

                    let doxylamineDataSeries = OCKDataSeriesConfiguration(
                        taskID: "doxylamine",
                        legendTitle: "Doxylamine",
                        gradientStartColor: .systemGray2,
                        gradientEndColor: .systemGray,
                        markerSize: 10,
                        eventAggregator: OCKEventAggregator.countOutcomeValues)

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

                    insightsCard.chartView.headerView.titleLabel.text = "Nausea & Doxylamine Intake"
                    insightsCard.chartView.headerView.detailLabel.text = "This Week"
                    insightsCard.chartView.headerView.accessibilityLabel = "Nausea & Doxylamine Intake, This Week"
                    listViewController.appendViewController(insightsCard, animated: false)

                    // Also create a card that displays a single event (nauseaCard).
                    // The event query passed into the initializer specifies that only
                    // today's log entries should be displayed by this log task view controller.
                    let nauseaCard = OCKButtonLogTaskViewController(
                        task: nauseaTask,
                        eventQuery: .init(for: date),
                        storeManager: self.storeManager)

                    listViewController.appendViewController(nauseaCard, animated: false)

                    // Add survey card
                    let surveyCard = SurveyViewController(
                        viewSynchronizer: SurveyViewSynchronizer(),
                        taskID: "survey",
                        eventQuery: OCKEventQuery(for: date),
                        //eventQuery: OCKEventQuery(for: Date()),
                        storeManager: self.storeManager)

                    listViewController.appendViewController(surveyCard, animated: false)

                }
            }
        }
    }
}

class ChartViewController: OCKDailyPageViewController {

    // This will be called each time the selected date changes.
    // It can be used as an opportunity to rebuild the content shown to the user.
    override func dailyPageViewController(_ dailyPageViewController: OCKDailyPageViewController,
                                          prepare listViewController: OCKListViewController, for date: Date) {
        // Define a query in order to search in the store for tasks related to the identifiers listed below, by date. Empty results are excluded.
        let identifiers = ["doxylamine", "nausea"]
        var query = OCKTaskQuery(for: date)
        query.ids = identifiers
        query.excludesTasksWithNoEvents = true

        // Apply the query to the store manager
        storeManager.store.fetchAnyTasks(query: query, callbackQueue: .main) { result in
            switch result {
            case .failure(let error): print("Error: \(error)")
            case .success(_):
                // If the query was executed without errors, the system continues to process the results, integrating them to the rest of the components.

                    // dynamic gradient colors to be used in the insights chart
                    let nauseaGradientStart = UIColor { traitCollection -> UIColor in
                        return traitCollection.userInterfaceStyle == .light ? #colorLiteral(red: 0.9960784314, green: 0.3725490196, blue: 0.368627451, alpha: 1) : #colorLiteral(red: 0.8627432641, green: 0.2630574384, blue: 0.2592858295, alpha: 1)
                    }
                    let nauseaGradientEnd = UIColor { traitCollection -> UIColor in
                        return traitCollection.userInterfaceStyle == .light ? #colorLiteral(red: 0.9960784314, green: 0.4732026144, blue: 0.368627451, alpha: 1) : #colorLiteral(red: 0.8627432641, green: 0.3598620686, blue: 0.2592858295, alpha: 1)
                    }

                    // Create a plot comparing nausea to medication adherence.
                    let nauseaDataSeries = OCKDataSeriesConfiguration(
                        taskID: "nausea",
                        legendTitle: "Nausea",
                        gradientStartColor: nauseaGradientStart,
                        gradientEndColor: nauseaGradientEnd,
                        markerSize: 10,
                        eventAggregator: OCKEventAggregator.countOutcomeValues)

                    let doxylamineDataSeries = OCKDataSeriesConfiguration(
                        taskID: "doxylamine",
                        legendTitle: "Doxylamine",
                        gradientStartColor: .systemGray2,
                        gradientEndColor: .systemGray,
                        markerSize: 10,
                        eventAggregator: OCKEventAggregator.countOutcomeValues)

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

                    insightsCard.chartView.headerView.titleLabel.text = "Nausea & Doxylamine Intake"
                    insightsCard.chartView.headerView.detailLabel.text = "This Week"
                    insightsCard.chartView.headerView.accessibilityLabel = "Nausea & Doxylamine Intake, This Week"
                    listViewController.appendViewController(insightsCard, animated: false)

            }
        }
    }
}

// 1. Subclass a task view controller to customize the control flow and present a ResearchKit survey
class SurveyViewController: OCKInstructionsTaskViewController, ORKTaskViewControllerDelegate {

    // 2. This method is called when the use taps the button!
    override func taskView(_ taskView: UIView & OCKTaskDisplayable, didCompleteEvent isComplete: Bool, at indexPath: IndexPath, sender: Any?) {

        // 2a. If the task was uncompleted, fall back on the super class's default behavior or deleting the outcome.
        if !isComplete {
            super.taskView(taskView, didCompleteEvent: isComplete, at: indexPath, sender: sender)
            return
        }

        // 2b. If the user attemped to mark the task complete, display a ResearchKit survey.
        let answerFormat = ORKAnswerFormat.scale(withMaximumValue: 10, minimumValue: 1, defaultValue: 5, step: 1, vertical: false, maximumValueDescription: "Very painful", minimumValueDescription: "No pain")
        let painStep = ORKQuestionStep(identifier: "pain", title: "Daily 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)
    }

    // 3. This method will be called when the user completes the survey.
    // Extract the result and save it to CareKit's store!
    func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
        taskViewController.dismiss(animated: true, completion: nil)
        guard reason == .completed else {
            taskView.completionButton.isSelected = false
            return
        }

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

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

    }
}

class SurveyViewSynchronizer: OCKInstructionsTaskViewSynchronizer {

    // Customize the initial state of the view
    override func makeView() -> OCKInstructionsTaskView {
        let instructionsView = super.makeView()
        instructionsView.completionButton.label.text = "Start survey"
        return instructionsView
    }

    // Customize the view updated
    override func updateView(_ view: OCKInstructionsTaskView,
                             context: OCKSynchronizationContext<OCKTaskEvents?>) {
        super.updateView(view, context: context)

        // Check if answer exists or not
        if let answer = context.viewModel?.firstEvent?.outcome?.values.first?.integerValue {
            view.headerView.detailLabel.text = "Pain level: \(answer)"
        } else {
            view.headerView.detailLabel.text = "Take survey"
        }
    }
}

struct MyView: UIViewControllerRepresentable {
    // Reading the store manager from the environment
    @EnvironmentObject private var storeManager: OCKSynchronizedStoreManager

    func makeUIViewController(context: Context) -> CareViewController {
        CareViewController(storeManager: storeManager)

    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {

    }
}

struct ContentView: View {

    var body: some View {
        MyView()

    }
}

struct ContentView_Previews: PreviewProvider {

    static var previews: some View {
        ContentView()
    }
}

struct AppView: View {

    var body: some View {
        TabView {
            ContentView()
                .tabItem {
                    Image(systemName: "house").renderingMode(.template)
                    Text("HOME")
                }
            VStack {
                        Text("Placeholder")
                            .font(.title)
                        Text("Log data")
                            .font(.subheadline)
                    }
            .tabItem {
                Image(systemName: "plus").renderingMode(.template)
                Text("LOG")
            }
        }
    }
}

struct AppView_Previews: PreviewProvider {

    static var previews: some View {
        AppView().environmentObject(AppDelegate.synchronizedStoreManager)
    }
}
erik-apple commented 3 years ago

It looks to me like the issue is that your survey card is looking for a task with the identifier "survey", but you have not added a task with that id to your store as part of the populateSampleData() method!

andreaxricci commented 3 years ago

Thanks @erik-apple! I confirm adding the task to the method helped solving the issue (I had to delete the app from the simulator and reinstall it, to refresh the store)

let survey = OCKTask(id: "survey", title: "Log data",carePlanID: nil, schedule: nauseaSchedule)
addTasks([nausea, doxylamine, kegels, survey], callbackQueue: .main, completion: nil)