HealthKit

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:

  1. collect and store health and fitness data
  2. analyze and visualize the data
  3. 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:

Steps to Use

  1. Create a new iOS App project in Xcode.
  2. Click the top entry in the Navigator.
  3. Select TARGETS ... {app-name} ... Info.
  4. Hover over one the entries and click the "+" button to add one.
  5. Add the key "Privacy - Health Share Usage Description"
  6. Enter a value like "This app needs to access your health data."
  7. Hover over one the entries and click the "+" button to add another.
  8. Add the key "Privacy - Health Update Usage Description"
  9. Enter a value like "This app needs to update your health data."
  10. Click the target under "TARGETS" which has the same name as the app.
  11. Click the "Signing & Capabilities" tab.
  12. Click "+ Capability".
  13. 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:

  1. Navigate to the "Signing & Capabilities" tab for the Target.
  2. Add a provisioning profile (see separate post)
  3. 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:

  1. Launch the Health app.
  2. Tap the "Sharing" button at the bottom.
  3. Tap "Apps".
  4. Tap the name of an app that wants permissions.
  5. 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

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)
    }
}