Overview
HealthKit is a library from Apple to "Access and share health and fitness data while maintaining the user’s privacy and control."
It supports three categories of tasks:
- collect and store health and fitness data
- analyze and visualize the data
- enable social interactions
"Because health data may contain sensitive, personal information, apps must receive permission from the user to read data from or write data to the HealthKit store."
Flutter
The Flutter pub.dev package health can access data from both Apple HealthKit and Android "Google Fit".
See my project flutter_health which is not yet working.
Available Data
The data available in HealthKit includes:
-
Activity (
HKQuantityType)activeEnergyBurnedappleExerciseTimeappleMoveTimeappleStandHour(HKCategoryTypeIdentifier)appleStandTimebasalEnergyBurneddistanceCyclingdistanceDownhillSnowSportsdistanceSwimmingdistanceWalkingRunningdistanceWheelchairflightsClimbed(stairs)lowCardioFitnessEvent(HKCategoryTypeIdentifier)nikeFuel(points earned)pushCount(wheelchair)stepCountswimmingStrokeCountvo2MaxwalkingSpeedwalkingStepLength
-
Blood (
HKQuantityType)bloodAlcoholContentbloodGlucosebloodPressureDiastolicbloodPressureSystolicoxygenSaturation
-
Body Measurements (
HKQuantityType)bodyFatPercentagebodyMassbodyMassIndexheightleanBodyMasswaistCircumference
-
Breathing (
HKQuantityType)forcedExpiratoryVolume1inhalerUsagepeakExpiratoryFlowRaterespiratoryRate
-
Characteristics (
HKCharacteristicType)activityMoveMode(TODO: What is this?)biologicalSexbloodTypedateOfBirthfitzpatrickSkinTypewheelchairUse
-
Dietary (
HKQuantityType)dietaryBiotindietaryCaffeinedietaryCalciumdietaryCarbohydratesdietaryChloridedietaryCholesteroldietaryChromiumdietaryCopperdietaryEnergyConsumeddietaryFatMonounsaturateddietaryFatPolyunsaturateddietaryFatSaturateddietaryFatTotaldietaryFiberdietaryFolatedietaryIodinedietaryIrondietaryMagnesiumdietaryManganesedietaryMolybdenumdietaryNiacindietaryPantothenicAciddietaryPhosphorusdietaryPotassiumdietaryProteindietaryRiboflavindietarySeleniumdietarySodiumdietarySugardietaryThiamindietaryVitaminAdietaryVitaminB12dietaryVitaminB6dietaryVitaminCdietaryVitaminDdietaryVitaminEdietaryVitaminAdietaryVitaminKdietaryWaterdietaryZinc
-
Hearing (
HKQuantityType)-
environmentalAudioExposure -
environmentalAudioExposureEvent(HKCategoryTypeIdentifier)"sent when the average sound level reaches or exceeds a specified threshold for three minutes"
-
headphoneAudioExposure -
headphoneAudioExposureEvent(HKCategoryTypeIdentifier)"when the device generates a notification about loud headphone audio"
-
-
Heart (
HKQuantityType)-
heartbeat series
-
heartRate -
heartRateVariabilitySDNN -
highHeartRateEvent(HKCategoryTypeIdentifier)For example, "Your heart rate rose above 120 BPM while you seemed to be inactive for 10 minutes starting at 10:06 AM."
-
irregularHeartRhythmEvent(HKCategoryTypeIdentifier)"might be suggestive of atrial fibrillation (AFib)"
-
lowHeartRateEvent(HKCategoryTypeIdentifier)This is similar to a
highHeartRateEvent. -
restingHeartRate -
walkingHeartRateAverage
-
-
Reproductive Health (
HKCategoryTypeIdentifier)basalBodyTemperature(HKQuantityType)cervicalMucusQualitycontraceptiveintermenstrualBleedinglactationmenstrualFlowovulationTestResultpregnancypregnancyTestResultprogesteroneTestResultsexualActivity
-
Vital Signs (
HKQuantityType)-
bodyTemperature -
electrocardiogram data
-
forcedVitalCapacityThis is the standard deviation of NN intervals.
-
-
Other (
HKQuantityType)-
appleWalkingSteadiness -
appleWalkingSteadinessEvent(HKCategoryTypeIdentifier)"an incident where the user showed a reduced score for their gait's steadiness"
-
electrodermalActivity -
insulinDelivery -
numberOfAlcoholicBeverages -
numberOfTimesFallen -
peripheralPerfusionIndex -
sixMinuteWalkTestDistance -
stairAscentSpeed -
stairDescentSpeed -
uvExposure -
walkingAsymmetryPercentage -
walkingDoubleSupportPercentage
-
Steps to Use
- Create a new iOS App project in Xcode.
- Click the top entry in the Navigator.
- Select TARGETS ... {app-name} ... Info.
- Hover over one the entries and click the "+" button to add one.
- Add the key "Privacy - Health Share Usage Description"
- Enter a value like "This app needs to access your health data."
- Hover over one the entries and click the "+" button to add another.
- Add the key "Privacy - Health Update Usage Description"
- Enter a value like "This app needs to update your health data."
- Click the target under "TARGETS" which has the same name as the app.
- Click the "Signing & Capabilities" tab.
- Click "+ Capability".
- Type "h" and double-click "HealthKit".
HealthKit cannot be used in the Simulator, so the app must be run on a real device.
Background Delivery
To enable background delivery of HealthKit events:
- Navigate to the "Signing & Capabilities" tab for the Target.
- Add a provisioning profile (see separate post)
- In the HealthKit section, check the "Background Delivery" checkbox
Permissions
The first time a user runs an app that uses HealthKit it will prompt for permission to access health data. Separate toggle switches are displayed for each kind of data to be written and each kind of data to be read. For example, a user can grant access to read their weight (a.k.a. bodyMass), but deny permission to write their weight.
If the user denies permission to access a particular kind of data and the app is run again later, it will not prompt the user for permission again.
To grant permission later:
- Launch the Health app.
- Tap the "Sharing" button at the bottom.
- Tap "Apps".
- Tap the name of an app that wants permissions.
- Enable/disable specific permissions or tap "Turn on all".
An error is thrown if an app attempts to write data for which the user has not granted permission.
Apps do not crash or throw an error if they attempt to read data for which the user has not granted permission. If the query is for a single value, ? is returned. If the query is for a sequence of data, an empty Array is returned.
Class Hierarchy
-
"The access point for all data managed by HealthKit."
-
"A piece of data that can be stored inside the HealthKit store."
This stores a UUID for the value (
uuid), the device that generated the data (device), the app that created the object (sourceRevision), and metadata in a map withStringkeys (metadata).-
"A HealthKit sample represents a piece of data associated with a start and end time."
-
"A sample with values from a short list of possible values."
-
"A sample that groups multiple related samples into a single entry."
-
"A sample that represents a quantity, including the value and the units."
-
"A sample that represents a cumulative quantity."
-
"A sample that represents a discrete quantity."
-
-
"A workout sample that stores information about a single physical activity."
This can include multiple values with different units such as meters run, flights of stairs climbed, and calories burned.
-
-
-
"An abstract superclass with subclasses that identify a specific type of data for the HealthKit store."
-
"A type that identifies activity summary objects."
-
"A type that represents data that doesn’t typically change over time."
Examples include birthday and blood type.
-
"An abstract superclass for all classes that identify a specific type of sample when working with the HealthKit store."
-
"An abstract superclass for all classes that identify a specific type of sample when working with the HealthKit store."
-
"A type that identifies samples that contain a value from a small set of possible values."
-
"A type that identifies samples that contain clinical record data."
-
"A type that identifies samples that group multiple subsamples."
-
"A sample type used to create queries for documents."
-
"A type that identifies samples containing electrocardiogram data."
-
"A type that identifies samples that store numerical values."
-
"A type that indicates the data stored in a series sample."
-
"A type that identifies samples that store information about a workout."
-
-
-
"An object that stores a value for a given unit."
The numeric value and unit this stores cannot be directly accessed. Instead call the
doubleValuemethod which returns the value after converting it to a specified unit. For example, the value may be stored in meters, but can be retrieved in miles. There is no need to writing code to do unit conversions. -
"An abstract class for all the query classes in HealthKit."
-
"A query for reading activity summary objects from the HealthKit store."
-
"A query that returns changes to the HealthKit store, including a snapshot of new changes and continuous monitoring as a long-running query."
-
"A query that performs complex searches based on the correlation’s contents, and returns a snapshot of all matching samples."
-
"A query that returns a snapshot of all matching documents currently saved in the HealthKit store."
-
"A query that returns the underlying voltage measurements for an electrocardiogram sample."
-
"A query that returns the heartbeat data contained in a heartbeat series sample."
-
"A long-running query that monitors the HealthKit store and updates your app when the HealthKit store saves or deletes a matching sample."
-
"A query that accesses the series data associated with a quantity sample."
-
"A general query that returns a snapshot of all the matching samples currently saved in the HealthKit store."
-
"A query that returns a list of sources, such as apps and devices, that have saved matching queries to the HealthKit store."
-
"A query that performs multiple statistics queries over a series of fixed-length time intervals."
-
"A query that performs statistical calculations over a set of matching quantity samples, and returns the results. You can use statistical queries to calculate the minimum, maximum, or average value of a set of discrete quantities, or use them to calculate the sum for cumulative quantities."
-
HKVerifiableClinicalRecordQuery
"A query for one-time access to a SMART Health Card."
-
"A query to access the location data stored in a workout route."
-
-
"A descriptor that specifies a set of samples based on the data type and a predicate."
-
"An object that represents the result of calculating the minimum, maximum, average, or sum over a set of samples from the HealthKit store."
Properties include
startDate,endDate,quantityType, andsources. Methods includeaverageQuantity,duration,maximumQuantity,minimumQuantity,mostRecentQuantity,mostRecentQuantityDateInterval, andsumQuantity. -
"An object that manages a collection of statistics, representing the results calculated over separate time intervals."
Duplicate data collected from multiple devices can be automatically ignored.
Reading Data
In order to access HealthKit, add the capability in the "Signing & Capabilities" tab of the main target.
Add the key "Privacy - Health Share Usage Description" in the "Info" tab of the main target with a description like "To read health data".
The following code demonstrates retrieving data from HealthKit and display it. For the full iOS project, see this GitHub repository.
// ContentView.swift
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = HealthKitViewModel()
func labelledValue(_ label: String, _ value: Double) -> some View {
Text("\(label): \(String(format: "%.0f", value))")
}
var body: some View {
VStack {
Text("Health Statistics for Past 7 Days")
.font(.title)
labelledValue("Average Heart Rate", viewModel.heartRate)
labelledValue(
"Average Resting Heart Rate",
viewModel.restingHeartRate
)
labelledValue("Total Steps", viewModel.steps)
labelledValue("Total Calories Burned", viewModel.activeEnergyBurned)
}
}
}
// HealthKitViewModel.swift
import HealthKit
@MainActor
class HealthKitViewModel: ObservableObject {
@Published private(set) var activeEnergyBurned: Double = 0
@Published private(set) var heartRate: Double = 0
@Published private(set) var restingHeartRate: Double = 0
@Published private(set) var steps: Double = 0
init() {
Task {
let healthManager = HealthManager()
do {
try await healthManager.authorize(identifiers: [
.activeEnergyBurned,
.heartRate,
.restingHeartRate,
.stepCount
])
let endDate = Date.now
let startDate = Calendar.current.date(
byAdding: DateComponents(day: -7),
to: endDate,
wrappingComponents: false
)!
activeEnergyBurned = try await healthManager.sum(
identifier: .activeEnergyBurned,
unit: .kilocalorie(),
startDate: startDate,
endDate: endDate
)
heartRate = try await healthManager.average(
identifier: .heartRate,
unit: HKUnit(from: "count/min"),
startDate: startDate,
endDate: endDate
)
restingHeartRate = try await healthManager.average(
identifier: .restingHeartRate,
unit: HKUnit(from: "count/min"),
startDate: startDate,
endDate: endDate
)
steps = try await healthManager.sum(
identifier: .stepCount,
unit: .count(),
startDate: startDate,
endDate: endDate
)
} catch {
print("error getting health data: \(error)")
}
}
}
}
// HealthManager.swift
import HealthKit
@MainActor
class HealthManager: ObservableObject {
private let store = HKHealthStore()
func authorize(identifiers: [HKQuantityTypeIdentifier]) async throws {
let typeSet: Set<HKQuantityType> = Set(
identifiers.map { .quantityType(forIdentifier: $0)! }
)
try await store.requestAuthorization(toShare: [], read: typeSet)
}
func average(
identifier: HKQuantityTypeIdentifier,
unit: HKUnit,
startDate: Date,
endDate: Date
) async throws -> Double {
try await withCheckedThrowingContinuation { completion in
let quantityType = HKQuantityType.quantityType(
forIdentifier: identifier
)!
let predicate: NSPredicate? = HKQuery.predicateForSamples(
withStart: startDate,
end: endDate
)
let query = HKStatisticsQuery(
quantityType: quantityType,
quantitySamplePredicate: predicate,
options: .discreteAverage
) { (_: HKStatisticsQuery, result: HKStatistics?, error: Error?) in
if let error {
completion.resume(throwing: error)
} else {
let quantity: HKQuantity? = result?.averageQuantity()
let result = quantity?.doubleValue(for: unit)
completion.resume(returning: result ?? 0)
}
}
store.execute(query)
}
}
func sum(
identifier: HKQuantityTypeIdentifier,
unit: HKUnit,
startDate: Date,
endDate: Date
) async throws -> Double {
try await withCheckedThrowingContinuation { completion in
let quantityType = HKQuantityType.quantityType(
forIdentifier: identifier
)!
let predicate: NSPredicate? = HKQuery.predicateForSamples(
withStart: startDate,
end: endDate
)
let query = HKStatisticsQuery(
quantityType: quantityType,
quantitySamplePredicate: predicate,
options: .cumulativeSum
) { (_: HKStatisticsQuery, result: HKStatistics?, error: Error?) in
if let error {
completion.resume(throwing: error)
} else {
let quantity: HKQuantity? = result?.sumQuantity()
let result = quantity?.doubleValue(for: unit)
completion.resume(returning: result ?? 0)
}
}
store.execute(query)
}
}
}
Writing Data
In order to access HealthKit, add the capability in the "Signing & Capabilities" tab of the main target.
Add the key "Privacy - Health Update Usage Description" in the "Info" tab of the main target with a description like "To write workout data".
The following code demonstrates writing data to HealthKit.
// ContentView.swift
import SwiftUI
// Code for this struct appears in the previous section.
struct ContentView: View {
...
@State private var caloriesBurned = "850"
@State private var cyclingMiles = "20.0"
@State private var endTime = Date() // adjusted in init
@State private var startTime = Date() // adjusted in init
init() {
// Remove seconds from the end time.
let calendar = Calendar.current
let endSeconds = calendar.component(.second, from: endTime)
let secondsCleared = calendar.date(
byAdding: .second,
value: -endSeconds,
to: endTime
)!
_endTime = State(initialValue: secondsCleared)
// Set start time to one hour before the end time.
let oneHourBefore = calendar.date(
byAdding: .hour,
value: -1,
to: endTime
)!
_startTime = State(initialValue: oneHourBefore)
}
// Add this method and call it when a Button is tapped.
private func addWorkout() {
Task {
do {
// HealthKit seems to round down to the nearest tenth.
// For example, 20.39 becomes 20.3.
// Adding 0.05 causes it to round to the nearest tenth.
let distance = (cyclingMiles as NSString).doubleValue + 0.05
let calories = (caloriesBurned as NSString).intValue
try await HealthKitManager().addCyclingWorkout(
startTime: startTime,
endTime: endTime,
distance: distance,
calories: Int(calories)
)
} catch {
print("Error adding workout: \(error)")
}
}
}
}
// HealthManager.swift
import HealthKit
// Code for this class appears in the previous section.
@MainActor
class HealthManager: ObservableObject {
...
// Add this method.
func addCyclingWorkout(
startTime: Date,
endTime: Date,
distance: Double,
calories: Int
) async throws {
let workout = HKWorkout(
activityType: HKWorkoutActivityType.cycling,
start: startTime,
end: endTime,
duration: 0, // compute from start and end data
totalEnergyBurned: HKQuantity(
unit: .kilocalorie(),
doubleValue: Double(calories)
),
totalDistance: HKQuantity(unit: .mile(), doubleValue: distance),
metadata: nil
)
try await store.save(workout)
}
}