CareKit Tutorial for iOS: Part 1
In the first part of our CareKit Tutorial for iOS, you’ll learn the basics of using CareKit by building a Zombie Training and Symptom Tracker. By Jeff Rames.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Assessment Activities
As you might have guessed, it’s time to eat some brai … Rather, it’s time to add some assessment activities to the store!
Open CarePlanData.swift and head over to init(carePlanStore:)
. Replace the line //TODO: Define assessment activities
with the following:
let pulseActivity = OCKCarePlanActivity
.assessment(withIdentifier: ActivityIdentifier.pulse.rawValue,
groupIdentifier: nil,
title: "Pulse",
text: "Do you have one?",
tintColor: UIColor.darkGreen(),
resultResettable: true,
schedule: CarePlanData.dailyScheduleRepeating(occurencesPerDay: 1),
userInfo: ["ORKTask": AssessmentTaskFactory.makePulseAssessmentTask()])
let temperatureActivity = OCKCarePlanActivity
.assessment(withIdentifier: ActivityIdentifier.temperature.rawValue,
groupIdentifier: nil,
title: "Temperature",
text: "Oral",
tintColor: UIColor.darkYellow(),
resultResettable: true,
schedule: CarePlanData.dailyScheduleRepeating(occurencesPerDay: 1),
userInfo: ["ORKTask": AssessmentTaskFactory.makeTemperatureAssessmentTask()])
You’ve defined two OCKCarePlanActivity
objects—one for pulse and one for temperature—using a convenience initializer that creates assessment activities. This required a couple parameters not used in the intervention activities created earlier:
-
resultResettable
takes a Boolean that indicates whether the user can re-take the assessment. When turned off, once data is entered, there’s no covering it up if you’re a zombie. - You place a dictionary in
userInfo
with the key"ORKTask"
. The value is a ResearchKit task (ORKTask) created by calling factory methods provided inAssessmentTaskFactory
, a struct included with the starter project. CareKit can leverage ResearchKit tasks for obtaining health data, and you’ll see shortly how these tasks are used when the user completes an assessment.
At the bottom of init(carePlanStore:)
, add your new tasks to the array you loop through when calling addActivity()
. It should now look like this:
for activity in [cardioActivity, limberUpActivity, targetPracticeActivity,
pulseActivity, temperatureActivity] {
add(activity: activity)
}
Build and run, and your new activities will appear in the Symptom Tracker tab. You’re now at 0% progress, because you haven’t completed either assessment yet. Note that as long as it isn’t the first day of the week, you can select and view prior days. Future days are never selectable.
There’s no putting it off—you might as well check to see if you’ve become a zombie. Select one of the assessments and … nothing happens!
Handling Input
On the Care Card, intervention activities have a binary input—the user simply taps an event on or off to indicate if it occurred. Assessments are more versatile; they can capture all types of data and thus require a variety of user interfaces. For this reason, you’re required to provide a view controller to handle assessment input.
ResearchKit provides a number of options for handling input of health data, and CareKit is designed to work well with them. As you may recall from the ResearchKit with Swift tutorial, input is gathered with ORKTaskViewController
objects which require ORKTask
objects. You’ve already bundled a task in the userInfo
of your assessment object—now it’s time to use it.
Open TabBarViewController.swift and add the following to the bottom of the file:
// MARK: - OCKSymptomTrackerViewControllerDelegate
extension TabBarViewController: OCKSymptomTrackerViewControllerDelegate {
func symptomTrackerViewController(_ viewController: OCKSymptomTrackerViewController,
didSelectRowWithAssessmentEvent assessmentEvent: OCKCarePlanEvent) {
guard let userInfo = assessmentEvent.activity.userInfo,
let task: ORKTask = userInfo["ORKTask"] as? ORKTask else { return }
let taskViewController = ORKTaskViewController(task: task, taskRun: nil)
//TODO: Set a delegate
present(taskViewController, animated: true, completion: nil)
}
}
The Symptom and Measurement Tracker view controller requires an OCKSymptomTrackerViewControllerDelegate
that must implement symptomTrackerViewController(_:didSelectRowWithAssessmentEvent:)
. This method is required to present a view controller for obtaining assessment data. It unwraps the ORKTask
you stored in userInfo
and uses that to initialize an ORKTaskViewController
, then present it.
Hop back up to createSymptomTrackerStack()
and add the following just after viewController
is allocated:
viewController.delegate = self
This sets the TabBarViewController
as the delegate, which means the extension you just defined will be used when an assessment is selected.
Build and run, then head to the Symptom Tracker tab. Select either Pulse or Temperature and…you’ll get a crash. If you look in the console, there will be a message similar to this:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'NSHealthShareUsageDescription must be set in the app's Info.plist in order to request read authorization.'
This is because ResearchKit tasks rely on HealthKit, and you haven’t yet enabled that capability. You’ll get to that in a bit, but for now you need to add the plist entry to appease this check.
Open Info.plist within ZombieKit and add the NSHealthShareUsageDescription key with the value I want to obtain your pulse and temperature data because zombies
:
You’ll access HealthKit data later in this tutorial, at which point the user will be prompted for authorization including the description you just provided.
Build and run, and once again select Pulse or temperature on the Symptom Tracker tab. Your ResearchKit controller now presents data from the task factory.
[ResearchKit][Warning]
. This is because ResearchKit wants to check HealthKit for the data, and you haven’t set that up yet.
If you try to hit Done or Cancel then End Task, nothing happens. This is because an ORKTaskViewController
requires a delegate callback to handle its results. Back in TabBarViewController.swift, add the following at the bottom of the file:
// MARK: - ORKTaskViewControllerDelegate
extension TabBarViewController: ORKTaskViewControllerDelegate {
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith
reason: ORKTaskViewControllerFinishReason, error: Error?) {
// 1
defer {
dismiss(animated: true, completion: nil)
}
// 2
guard reason == .completed else { return }
guard let symptomTrackerViewController = symptomTrackerViewController,
let event = symptomTrackerViewController.lastSelectedAssessmentEvent else { return }
//TODO: convert ORKTaskResult to CareKit result and add to store
}
}
- This dismisses the task view controller in a defer block that will execute after handling the results.
- Upon successful completion, you unwrap
symptomTrackerViewController.lastSelectedAssessmentEvent
to gain access to the event tied to the task that triggered the current task flow. In a moment, you’ll use this reference to save the results to the store.
You can’t simply save a ResearchKit task result into CareKit—you need to first convert it. Open CarePlanStoreManager.swift and import ResearchKit:
import ResearchKit
Then add the following method to CarePlanStoreManager
:
func buildCarePlanResultFrom(taskResult: ORKTaskResult) -> OCKCarePlanEventResult {
// 1
guard let firstResult = taskResult.firstResult as? ORKStepResult,
let stepResult = firstResult.results?.first else {
fatalError("Unexepected task results")
}
// 2
if let numericResult = stepResult as? ORKNumericQuestionResult,
let answer = numericResult.numericAnswer {
return OCKCarePlanEventResult(valueString: answer.stringValue, unitString: numericResult.unit, userInfo: nil)
}
// 3
fatalError("Unexpected task result type")
}
This converts a ResearchKit result into a CareKit result.
- The
ORKTaskResult
s you created have a single step, so you pull the first and only one intofirstResult
. Next you grab the first and only question result of this step, which will contain the data you collected. - You cast the result as a
ORKNumericQuestionResult
, which is the only result type your tasks collect. You then grab the answer and pass it as the value when creating and returning anOCKCarePlanEventResult
. - Remember that you only call this method when a task successfully completes, so if it doesn’t contain a value, you’re simply failing.
Return to TabBarViewController.swift and replace //TODO: convert ORKTaskResult to CareKit result and add to store
with:
let carePlanResult = carePlanStoreManager.buildCarePlanResultFrom(taskResult: taskViewController.result)
carePlanStoreManager.store.update(event, with: carePlanResult, state: .completed) {
success, _, error in
if !success {
print(error?.localizedDescription)
}
}
This passes your ResearchKit task result to buildCarePlanResultFrom(taskResult:)
and gets back an OCKCarePlanEventResult
. You pass that carePlanResult
along with the current event to update(:_with:state:completion:)
to commit it to the store. You print an error message if it was unsuccessful.
Finally, head back to symptomTrackerViewController(_:didSelectRowWithAssessmentEvent:)
and replace //TODO: Set a delegate
with:
taskViewController.delegate = self
Now when the user submits a completed ResearchKit task, your new delegate method will be invoked to convert and save it.
Build and run. Now when you complete an assessment, the task controller will dismiss, your care store will update, and the symptom tracker will update to reflect changes to the completion percent and recorded value.