Reading data from HealthKit in a SwiftUI app

Reading data from HealthKit in a SwiftUI app

Learn how to access and read data stored in the Health app in a SwiftUI app.

Apple’s HealthKit framework allows developers to access health and fitness data stored on a user’s iPhone or Apple Watch. Developers can access this data to create practical applications that creates additional value to our products.

In this tutorial, we’ll build a complete SwiftUI app that reads step count, active energy burned, and heart rate.

By the end of this tutorial, you will have created a fully functional SwiftUI app that requests authorization to access HealthKit data, fetches today’s step count and calories burned, along with the most recent heart rate, and displays this information in a clean and simple UI.

Before we start

To follow this tutorial, you need a basic understanding of SwiftUI and be comfortable writing code using the Swift programming language. We will start from an empty project in SwiftUI for iOS.

Since privacy is a major concern for Apple, especially for accessing health information, we need to add the HealthKit capability to our Xcode project:

Health information is sensitive. To be able to access it you must request user authorization. In the Info tab of the app settings, add a new field called “Privacy - Health Share Usage Description”, with a textual description explaining why our app requires this permission.

Step 1 - Managing authorization and data fetching

The first step is to create a Swift class that will manage authorization requests and data retrieval from the HealthKit framework. We'll define a new file named HealthKitManager.swift, which contains a singleton class to encapsulate all HealthKit-related logic, including requesting permissions and fetching health data.

import Foundation
import HealthKit

@MainActor
class HealthKitManager {

	// 1.
    static let shared = HealthKitManager()
    private let healthStore = HKHealthStore()

    private init() {}

    // 2.
    func requestAuthorization() async throws -> Bool {
        // Ensure HealthKit is available on this device
        guard HKHealthStore.isHealthDataAvailable() else { return false }

        // Define the types we want to read
        let readTypes: Set<HKObjectType> = [
            HKObjectType.quantityType(forIdentifier: .stepCount)!,
            HKObjectType.quantityType(forIdentifier: .heartRate)!,
            HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!
        ]

        return try await withCheckedThrowingContinuation { continuation in
            healthStore.requestAuthorization(toShare: [], read: readTypes) { success, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: success)
                }
            }
        }
    }

    // 3.
    func fetchMostRecentSample(for identifier: HKQuantityTypeIdentifier) async throws -> HKQuantitySample? {
        // Get the quantity type for the identifier
        guard let quantityType = HKObjectType.quantityType(forIdentifier: identifier) else {
            return nil
        }

        // Query for samples from start of today until now, sorted by end date descending
        let predicate = HKQuery.predicateForSamples(
            withStart: Calendar.current.startOfDay(for: Date()),
            end: Date(),
            options: .strictStartDate
        )
        let sortDescriptor = NSSortDescriptor(
            key: HKSampleSortIdentifierEndDate,
            ascending: false
        )

        return try await withCheckedThrowingContinuation { continuation in
            let query = HKSampleQuery(
                sampleType: quantityType,
                predicate: predicate,
                limit: 1,
                sortDescriptors: [sortDescriptor]
            ) { _, samples, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: samples?.first as? HKQuantitySample)
                }
            }
            healthStore.execute(query)
        }
    }
}
  1. We start by creating two important properties inside the HealthKitManager class:
    1. A singleton instance (shared) that provides a globally accessible reference to this manager across the app.
    2. An instance of HKHealthStore, which is the main interface for interacting with the HealthKit framework, including requesting authorization and querying health data.
  2. In the requestAuthorization() method, we first ensure that HealthKit is available on the device. We then specify the types of health data we want to read such as step count, heart rate, and active energy burned. Using HealthKit's authorization API, we prompt the user to grant permission, and handle the result asynchronously.
  3. After authorization is granted, the fetchMostRecentSample() method is defined to retrieve the most recent data entry for a specific HealthKit quantity type (e.g., steps or heart rate). To do this, we construct an HKSampleQuery, which requires:
    1. The quantity type to query (e.g., .stepCount).
    2. A predicate to filter results (e.g., from the start of the day to now).
    3. A sort descriptor to order results by end date in descending order, so the most recent sample is returned.

Step 2 - Define the ViewModel

Now that we’ve defined our HealthKitManager, the next step is to create a new file called HealthDataViewModel.swift. This class will serve as a bridge between the view layer and the data layer, managing the health data state and handling user authorization flow.

import Foundation
import HealthKit
import Observation

@MainActor
@Observable class HealthDataViewModel {
	
    // 1.
    var stepCount: Double = 0
    var heartRate: Double = 0
    var activeEnergy: Double = 0
    var isAuthorized: Bool = false
    var errorMessage: String?

    init() {
        Task { await requestAuthorization() }
    }

    // 2.
    func requestAuthorization() async {
        do {
            let success = try await HealthKitManager.shared.requestAuthorization()
                self.isAuthorized = success
            if success {
                await fetchAllHealthData()
            }
        } catch {
            self.errorMessage = error.localizedDescription 
        }
    }

    // 3.
    func fetchAllHealthData() async {
        async let steps: () = fetchStepCount()
        async let rate: ()  = fetchHeartRate()
        async let energy: () = fetchActiveEnergy()
        _ = await (steps, rate, energy)
    }

    // 4.
    func fetchStepCount() async {
        if let sample = try? await HealthKitManager.shared.fetchMostRecentSample(for: .stepCount) {
            let value = sample.quantity.doubleValue(for: HKUnit.count())
            self.stepCount = value
        }
    }

    func fetchHeartRate() async {
        if let sample = try? await HealthKitManager.shared.fetchMostRecentSample(for: .heartRate) {
            let value = sample.quantity
                .doubleValue(for: HKUnit.count().unitDivided(by: HKUnit.minute()))
            self.heartRate = value
        }
    }

    func fetchActiveEnergy() async {
        if let sample = try? await HealthKitManager.shared.fetchMostRecentSample(for: .activeEnergyBurned) {
            let value = sample.quantity.doubleValue(for: HKUnit.kilocalorie())
            self.activeEnergy = value
        }
    }
}

  1. In the HealthDataViewModel we define all the properties to store the latest values for step count, heart rate, and active energy burned.
  2. When the view model is initialized, it immediately attempts to request HealthKit authorization using the requestAuthorization() method. If the permission is granted, it proceeds to fetch the health data.
  3. The fetchAllHealthData() method uses the async let syntax to fetch all three health metrics concurrently, improving the efficiency by running the fetch operations in parallel instead of sequentially.
  4. Each fetch method calls the HealthKitManager to request the most recent sample for a specific health metric (e.g., .stepCount, .heartRate). Once retrieved, the raw quantity is converted into a Double using the appropriate HealthKit unit (e.g., .count(), .kilocalorie()), and the result is stored in the corresponding property.

Step 3 - Display all the information in a SwiftUI view

With the HealthDataViewModel now managing all authorization and data retrieval from HealthKit, the final step is to present this information to the user in a SwiftUI view. Open the ContentView.swift file and update it as shown below:


import SwiftUI

struct ContentView: View {

    // 1.
    @State private var viewModel = HealthDataViewModel()

    // 2.
    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                if let error = viewModel.errorMessage {
                    Text("Error: \(error)")
                        .foregroundColor(.red)
                } else if viewModel.isAuthorized {
                    VStack(spacing: 16) {
                            HealthInfoView(
                                label: Text("Step Count"),
                                value: Text("\(Int(viewModel.stepCount)) steps")
                            )

                            HealthInfoView(
                                label: Text("Heart Rate"),
                                value: Text(String(format: "%.1f bpm", viewModel.heartRate)),
                                color: .red
                            )

                            HealthInfoView(
                                label: Text("Active Energy"),
                                value: Text(String(format: "%.1f kcal", viewModel.activeEnergy)),
                                color: .green
                            )
                        }
                } else {
                    ProgressView("Requesting HealthKit authorization...")
                                            .padding()
                }
            }
            .navigationTitle("Health Data")
        }
    }
}

struct HealthInfoView<Label: View, Value: View>: View {
    let label: Label
    let value: Value
    var color: Color = .orange

    var body: some View {
        RoundedRectangle(cornerRadius: 25)
            .fill(color.gradient)
            .frame(width: 200, height: 150)
            .overlay {
                VStack {
                    label
                    value
                }
                .font(.title2)
                .fontWeight(.bold)
                .foregroundColor(.white)
                .padding()
            }
    }
}


#Preview {
    ContentView()
}

  1. This view uses a @State variable to create and manage an instance of HealthDataViewModel, which keeps track of the latest health metrics and authorization state.
  2. The UI adapts based on the app's current state:
    1. If an error occurred during authorization or data fetching, it displays the error message.
    2. If authorization is granted, it shows the most recent values for step count, heart rate, and active energy burned.
    3. If authorization is still pending, a loading message is displayed.

Conclusion

In this tutorial, we learned how to integrate HealthKit into a SwiftUI application using a structured and modular approach. This enables our app to access health-related data. When the app is launched, a system modal prompts the user to grant permission to share their health information. Once permission is granted, the app can access and utilize the requested data.

0:00
/0:16

Here's the final project that you can download with all the code included in the article: