Accessing Activity Rings Data from HealthKit

In the most recent updates to Streaks and HealthFace, we've added the ability to see your current Activity Rings data.

Although on iOS/watchOS the activity rings are made up of active energy burned, exercise minutes and stand hours - each of which has its own HealthKit category - the actual "Activity Rings" has its own HealthKit category also.

In this article I will show you how to do the following:

  1. Query activity rings data for the current day
  2. Draw activity rings in your app
  3. Be notified when activity ring data is updated

Querying Activity Rings Data

While you can manually retrieve the current values for active energy burned, exercise minutes and stand hours from their corresponding HealthKit categories, it's not possible to find out the daily target.

At the time of writing, it's not possible to change from a stand target of 12 hours or an exercise target of 30 minutes, but it is possible to change the active energy burned target.

To retrieve all six values at once (the current value and the target for each of the rings), you can use HKActivitySummaryQuery. This query is used to retrieve one or more days of activity ring data.

First, you need to request access to read the activity rings data. This is required for HKActivitySummaryQuery to succeed.

let healthStore = HKHealthStore()

let objectTypes: Set<HKObjectType> = [
    HKObjectType.activitySummaryType()
]

healthStore.requestAuthorization(toShare: nil, read: objectTypes) { (success, error) in

    // Authorization request finished, hopefully the user allowed access!
}

Remember that you should only create one instance of HKHealthStore in your app, and re-use it for all queries and operations.

To retrieve the activity rings for the current day, the following predicate can be used:

let calendar = Calendar.autoupdatingCurrent
        
var dateComponents = calendar.dateComponents(
    [ .year, .month, .day ],
    from: Date()
)

// This line is required to make the whole thing work
dateComponents.calendar = calendar

let predicate = HKQuery.predicateForActivitySummary(with: dateComponents)

Alternatively, you can retrieve activity ring information for a range of dates using HKQuery.predicate(forActivitySummariesBetweenStart:end:).

Once you have created the predicate, you can retrieve the information as follows:

let query = HKActivitySummaryQuery(predicate: predicate) { (query, summaries, error) in

    guard let summaries = summaries, summaries.count > 0
    else {
        // No data returned. Perhaps check for error
        return
    }

    // Handle the activity rings data here
}

In the above example, summaries is an array of HKActivitySummary.

To actually extract the values, you need to decide on which units to use. Even though stand hours sounds like a duration, it's returned as a count value, indicating the number of hours.

let energyUnit   = HKUnit.jouleUnit(with: .kilo)
let standUnit    = HKUnit.count()
let exerciseUnit = HKUnit.second()

Alternatively, you may want to retrieve the energy in kilocalories instead:

let energyUnit = HKUnit.kilocalorie()

You can now retrieve the current values:

let energy   = summary.activeEnergyBurned.doubleValue(for: energyUnit)
let stand    = summary.appleStandHours.doubleValue(for: standUnit)
let exercise = summary.appleExerciseTime.doubleValue(for: exerciseUnit)

Likewise, you can retrieve the targets:

let energyGoal   = summary.activeEnergyBurnedGoal.doubleValue(for: energyUnit)
let standGoal    = summary.appleStandHoursGoal.doubleValue(for: standUnit)
let exerciseGoal = summary.appleExerciseTimeGoal.doubleValue(for: exerciseUnit)

You can then determine the progress as a percentage:

let energyProgress   = energyGoal == 0 ? 0 : energy / energyGoal
let standProgress    = standGoal == 0 ? 0 : stand / standGoal
let exerciseProgress = exerciseGoal == 0 ? 0 : exercise / exerciseGoal

Note the basic check to avoid division by zero!

You can now plug these lines into the HKActivitySummaryQuery call above - don't forget the execute the query:

healthStore.execute(query)

Drawing Activity Rings

In both Streaks and HealthFace we are manually drawing the activity rings, since we have specific requirements. However, Apple make your life easy by providing a class called HKActivityRingView : UIView that you can use to easily display activity rings!

Note: You need to import HealthKitUI in order to access this class.

Using HKActivityRingView, you can animate to the progress rings. It makes use of the HKActivitySummary as used above.

import HealthKitUI

// Create the view with a size of 200x200
let frame    = CGRect(x: 0, y: 0, width: 200, height: 200)
let ringView = HKActivityRingView(frame: frame)

// Assume we're in a UIViewController with a self.view property.
self.view.addSubview(ringView)

// Retrieve the current summary
let summary: HKActivitySummary = ... // Retrieve from query above

// Update the view to display the current summary
ringView.setActivitySummary(summary, animated: true)

You can animate from the view's current ring state to the new values by passing true to animated.

When you run this code, you will see something similar to the following:

Being Notified of Activity Ring Updates

When using HealthKit on iOS, it is possible to subscribe to new data being recorded into the HealthKit database, even while your app is in the background. This is achieved using the HKObserverQuery query type.

Note: Why is this useful? You can trigger notifications, update Today Extension state, notify your Apple Watch app, whatever you like!

Unfortunately, it is not possible to use HKObjectType.activitySummaryType() with HKObserverQuery. Instead, you can subscribe to changes for active energy burned, exercise minutes and stand hours.

The following code shows how to create queries for each of these types and then enable background delivery when this data is changed.

let types = [
    HKObjectType.categoryType(forIdentifier: .appleStandHour)!,
    HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
    HKObjectType.quantityType(forIdentifier: .appleExerciseTime)!
]

for type in types {
    let query = HKObserverQuery(sampleType: type, predicate: nil) { (query, completionHandler, error) in

        // Handle new data here

    }

    healthStore.execute(query)
    healthStore.enableBackgroundDelivery(for: type, frequency: .immediate) { (complete, error) in

    }
}

Now when data has been changed - whether your app is in the foreground or background - you will be notified so the rings can be updated accordingly.