CareKit Tutorial for iOS: Part 2

In the second part of our CareKit tutorial, you’ll learn how to use Insights and Connect to build an iOS app that helps users manage and understand their personal health. By Jeff Rames.

Leave a rating/review
Save for later
Share

Welcome to the second and final installment of our CareKit Tutorial series. Now with more zombies!

In Part 1, you learned that CareKit primarily consists of four UI modules and a persistence store. You built an app called ZombieKit and implemented the Care Plan Store, a Care Card and a Symptom and Measurement Tracker.

In this second half of the CareKit tutorial, you’ll build on ZombieKit by implementing the last two UI modules:

  • Insights will be used to visualize patterns in the data collected by the Care Card and the Symptom and Measurement Tracker.
  • Connect will allow you to display contacts involved in the user’s care plan and share data with them.

Insights will require lots of asynchronous calls to read from the Care Plan Store. If you don’t have much experience with multithreading, Grand Central Dispatch and especially Dispatch Groups, consider reading Grand Central Dispatch Tutorial Part 1 and Part 2 before proceeding.

You’ll pick up exactly where you left off, so open up your completed project from Part 1, or download the Part 1 final project.

Note: If you download the final project, you’ll optionally need to enable HealthKit, because access to your development account is required for that. Follow the instructions under HealthKit Integration in Part 1.

Time to re-join the fray and try to code away the zombie epidemic!

Insights

In Part 1, you learned that OCKCarePlanActivity is the primary model object in the Care Plan Store, representing user activities and everything needed to display them. Each occurrence of an activity is defined by an OCKCarePlanEvent, and data are persisted to the Care Plan Store as events are completed.

Insights help the user make conclusions from the data. Two output formats are supported:

CareKit tutorial

CareKit tutorial

  • Charts: CareKit currently only supports bar charts, which can be grouped for comparing data and visualizing patterns. In a weight-loss app, you’d likely chart adherence to exercise goals against weight to visualize the correlation over time.
  • Messages: These are simple views with a title and some detail text that come as either tips or alerts. The only difference between the types is the appearance of the icon by the message title. For the weight-loss app, you might include an alert informing the user if they are missing their goal.

Insights reads from Intervention and Assessment activities in the store. However, while it’s designed to work with CareKit, Insights can display any data you like.

CareKit tutorial

Insights View Controller

Users of ZombieKit should be able to track their training adherence against their vital signs to judge how successful their care plan is. You’ll start by getting the Insights controller going with something simple.

Create a new file named OCKInsightItem.swift in the Utilities group. Replace the template code with the following:

import CareKit

extension OCKInsightItem {
  static func emptyInsightsMessage() -> OCKInsightItem {
    let text = "You haven't entered any data, or reports are in process. (Or you're a zombie?)"
    return OCKMessageItem(title: "No Insights", text: text,
                          tintColor: UIColor.darkOrange(), messageType: .tip)
  }
}

You’ve extended OCKInsightItem, the base class from which message and chart insights inherit. emptyInsightsMessage() creates and returns an OCKMessageItem. It includes a placeholder title and message and uses a tip messageType, which places a tinted asterisk by the title.

Open TabBarViewController.swift and add the following to the properties in TabBarViewController:

fileprivate var insightsViewController: OCKInsightsViewController? = nil

OCKInsightsViewController is the main controller for Insights, and you’ll reference it throughout this file.

In createInsightsStack(), replace:

let viewController = UIViewController()

with:

let viewController = OCKInsightsViewController(insightItems: [OCKInsightItem.emptyInsightsMessage()],
                                               headerTitle: "Zombie Check", headerSubtitle: "")
insightsViewController = viewController

The initializer for OCKInsightsViewController requires an array of OCKInsightItem objects, so you wrap the result of OCKInsightItem.emptyInsightsMessage() in an array. You also provide a headerTitle to appear at the top of the Insight view. Finally, you save a reference to the controller in insightsViewController for later use.

Build and run, then check out the Insights tab to see the header text and your placeholder tip.

CareKit tutorial

Contrary to the name, this isn’t all that insightful yet. It’s time to get your survivor’s data in here!

Completion Data

To generate meaningful insight items, you’ll gather and process data from the Care Plan Store. Start by calculating what percentage of the training plan was completed each day.

Create a new file named InsightsDataManager.swift in the Care Plan Store group. Replace the template code with:

import CareKit

class InsightsDataManager {
  let store = CarePlanStoreManager.sharedCarePlanStoreManager.store
  var completionData = [(dateComponent: DateComponents, value: Double)]()
  let gatherDataGroup = DispatchGroup()
}

InsightsDataManager is responsible for pulling data from the Care Plan Store and generating insights from it. You’ve defined the following properties:

  • store references your Care Plan Store.
  • completionData is an array of tuples tying a date to its corresponding intervention completion value.
  • gatherDataGroup is a dispatch group you’ll use to control the order in which several asynchronous operations complete during data processing.

Next, add the following method:

func fetchDailyCompletion(startDate: DateComponents, endDate: DateComponents) {
  // 1
  gatherDataGroup.enter()
  // 2
  store.dailyCompletionStatus(
    with: .intervention,
    startDate: startDate,
    endDate: endDate,
    // 3
    handler: { (dateComponents, completed, total) in
      let percentComplete = Double(completed) / Double(total)
      self.completionData.append((dateComponents, percentComplete))
    },
    // 4
    completion: { (success, error) in
      guard success else { fatalError(error!.localizedDescription) }
      self.gatherDataGroup.leave()
  })
}

This calculates the percentage of Intervention events completed for each day in the specified date range.

  1. Before kicking off the query, you enter the gatherDataGroup dispatch group. Later in this tutorial, you’ll add additional queries that will occur concurrently with this one—the group allows you to track when they all complete.
  2. You have the Care Plan Store method dailyCompletionStatus(with:startDate:endDate:handler:completion:) query all intervention activities in the date range for completion data.
  3. The handler closure is called once for each day in the range, and is passed several pieces of information pertaining to that day: dateComponents has the date, completed is a count of events that were completed on that date and total is a count of events in any state. From this, you calculate the daily completion percentage and save it with the date in completionData.
  4. The completion closure is called after all days in the range return. You halt execution in the case of a failure. On success, you leave the gatherDataGroup which will later clear the way for insight creation to proceed.

To control the flow of data collection through insight creation, add the following to InsightsDataManager:

func updateInsights(_ completion: ((Bool, [OCKInsightItem]?) -> Void)?) {
  guard let completion = completion else { return }
  
  // 1
  DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
    // 2
    let startDateComponents = DateComponents.firstDateOfCurrentWeek
    let endDateComponents = Calendar.current.dateComponents([.day, .month, .year], from: Date())
    
    //TODO: fetch assessment data
    self.fetchDailyCompletion(startDate: startDateComponents, endDate: endDateComponents)
    
    // 3
    self.gatherDataGroup.notify(queue: DispatchQueue.main, execute: { 
      print("completion data: \(self.completionData)")
      completion(false, nil)
    })
  }
}

This will be used as an interface to kick off Insight data operations, and includes a closure that accepts an array of OCKInsightItems.

  1. Because this method will kick off asynchronous database calls and do computations on the results, it needs to happen on a background queue.
  2. You call fetchDailyCompletion(startDate:endDate:) with the start date of the current week and the current date. This will populate completionData with the results.
  3. notify(queue:execute:) defines a completion closure that will run on the main queue when all operations running under gatherDataGroup complete. This means it will fire when the completion data has been fetched and processed. In the closure, you temporarily print the fetched data and pass nil data to the completion—you’ll replace this later.

In Part 1, you created CarePlanStoreManager to handle Care Plan Store operations; this is where you’ll create an interface to the InsightsDataManager. Open CarePlanStoreManager.swift and add the following to CarePlanStoreManager:

func updateInsights() {
  InsightsDataManager().updateInsights { (success, insightItems) in
    guard let insightItems = insightItems, success else { return }
    //TODO: pass insightItems to the insights controller
  }
}

This calls updateInsights(_:) in the InsightsDataManager and applies a guard in the completion closure to unwrap the results when successful. Later, you’ll pass these to the Insights controller for display.

To ensure insights are ready when the user pulls them up, you refresh them every time the store is updated. Add the following to the bottom of the file:

// MARK: - OCKCarePlanStoreDelegate
extension CarePlanStoreManager: OCKCarePlanStoreDelegate {
  func carePlanStore(_ store: OCKCarePlanStore, didReceiveUpdateOf event: OCKCarePlanEvent) {
    updateInsights()
  }
}

You’ve implemented a delegate method that the OCKCarePlanStore calls whenever an event is updated. You use it to keep Insights updated via updateInsights().

Go to init() in CarePlanStoreManager and add this line just below super.init():

store.delegate = self

CarePlanStoreManager is now the store’s delegate, so your new extension method will trigger.

Take a deep breath (assuming you’re still among the living). It wasn’t easy, but you’ve got all the bones here to start cranking out Insights!

Build and run, then tap on some training events in Zombie Training or complete an activity in the Symptom Tracker. Watch the console—you’ll see updated completionData from your logging. This means you’ve successfully received notifications of event updates, queried the events and calculated completion percentages.

Completion percentage in Double format

Completion percentage in Double format

Now that you have completion data by date, it’s time to pretty it up a bit in chart form!

Contributors

Over 300 content creators. Join our team.