How do HealthKit units and aggregation styles work, and why does converting mg/dL to mmol/L crash?

17.10.3 · L5 · Interview

Predict first

Do glucose units convert?

import Foundation
import HealthKit

let mgPerDl = HKUnit(from: "mg/dL")
let mmolPerL = HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter())
let glucose = HKQuantity(unit: mgPerDl, doubleValue: 100)
print("compatible with mmol/L:", glucose.is(compatibleWith: HKUnit(from: "mmol/L")), "| with a molar-mass unit:", glucose.is(compatibleWith: mmolPerL))
print("100 mg/dL =", (glucose.doubleValue(for: mmolPerL) * 100).rounded() / 100, "mmol/L")
let lb = HKQuantity(unit: .pound(), doubleValue: 150)
print("150 lb =", (lb.doubleValue(for: .gramUnit(with: .kilo)) * 100).rounded() / 100, "kg")
func style(_ t: HKQuantityType) -> String {
    switch t.aggregationStyle {
    case .cumulative: return "cumulative (sum)"
    case .discreteArithmetic: return "discrete (average)"
    case .discreteTemporallyWeighted: return "discrete, time-weighted"
    default: return "other"
    }
}
for (name, id) in [("steps", HKQuantityTypeIdentifier.stepCount), ("heart rate", .heartRate), ("body mass", .bodyMass), ("active energy", .activeEnergyBurned)] {
    print(name, "->", style(HKQuantityType(id)))
}
print("heart rate takes count/min:", HKQuantityType(.heartRate).is(compatibleWith: HKUnit.count().unitDivided(by: .minute())), "| metres:", HKQuantityType(.heartRate).is(compatibleWith: .meter()))
  • mg/dL is not compatible with plain mmol/L but is with a molar-mass unit; steps are cumulative, heart rate time-weighted
  • All units convert freely
  • Heart rate is cumulative

Predict first

What happens when the units are incompatible?

import HealthKit
let q = HKQuantity(unit: HKUnit(from: "mg/dL"), doubleValue: 100)
print(q.doubleValue(for: HKUnit(from: "mmol/L")))
  • The process crashes with an uncaught Objective-C exception
  • It returns 5.55
  • It returns 0

Mental model. Millilitres and grams do not convert until you say what the liquid is; molar mass is the density of the conversion.

An HKQuantity pairs a value with an HKUnit, and conversion is only legal between compatible units: pounds convert to kilograms (150 lb is 68.04 kg), but mg/dL (mass per volume) and mmol/L (moles per volume) are not compatible unless you build the mole unit with the substance’s molar mass: HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose) then 100 mg/dL is 5.55 mmol/L. Converting between incompatible units raises an Objective-C exception (Attempt to convert incompatible units), which Swift cannot catch, so check is(compatibleWith:) first. Each quantity type also has an aggregation style: steps and active energy are cumulative (sum them), heart rate is discrete and time-weighted, body mass is discrete (average), and the type decides which statistics options a query may use.

Authorization is per type and, by design, you cannot tell whether read access was denied: an empty result looks the same as no data (Q379). Store samples in canonical units and show the user’s preferred units (preferredUnits(for:)). The unit maths ran on macOS; authorization, queries and device data were not run.

Compatibility and aggregation style are properties of the types.

The conversion raises NSInvalidArgumentException.

Common traps

  • Converting incompatible units. It throws an uncatchable exception; test compatibility first.
  • Summing a discrete type. Heart rate must be averaged or time-weighted.

They might follow up with

  • How do I know if the user denied read access? You cannot, by design.

Related questions

  • 17.3.2 How does HealthKit handle authorization and units, and what should an app know about Core Motion and sensor…

← How is an EventKit event modelled: recurrence rules, alarms…   How do you detect which sensors and health data exist at runtime, and… →