```html SwiftUI: How to Implement HealthKit (iOS 17+, 2026)

How to Implement HealthKit in SwiftUI

iOS 17+ Xcode 16+ Advanced APIs: HealthKit Updated: May 12, 2026
TL;DR

Create an HKHealthStore, call requestAuthorization(toShare:read:) with async/await, then run an HKStatisticsQuery wrapped in a withCheckedContinuation to pull today's step count into your SwiftUI view.

import HealthKit
import SwiftUI

struct StepCountView: View {
    @State private var steps = 0
    private let store = HKHealthStore()

    var body: some View {
        Text("\(steps) steps today")
            .task { await fetchSteps() }
    }

    private func fetchSteps() async {
        guard HKHealthStore.isHealthDataAvailable() else { return }
        let stepType = HKQuantityType(.stepCount)
        try? await store.requestAuthorization(toShare: [], read: [stepType])
        let now = Date()
        let start = Calendar.current.startOfDay(for: now)
        let pred = HKQuery.predicateForSamples(withStart: start, end: now)
        steps = await withCheckedContinuation { cont in
            let q = HKStatisticsQuery(
                quantityType: stepType,
                quantitySamplePredicate: pred,
                options: .cumulativeSum
            ) { _, stats, _ in
                cont.resume(returning: Int(stats?.sumQuantity()?.doubleValue(for: .count()) ?? 0))
            }
            store.execute(q)
        }
    }
}

Full implementation

The approach below encapsulates all HealthKit logic inside an @Observable class — the iOS 17 replacement for ObservableObject — keeping your views thin. Authorization is requested once on launch via the .task modifier, then three HKStatisticsQuery calls run concurrently with Swift's structured concurrency (async let). Before this compiles you must enable the HealthKit capability in your target and add NSHealthShareUsageDescription to Info.plist.

import HealthKit
import SwiftUI

// MARK: - Manager

@Observable
final class HealthKitManager {
    var stepCount: Int = 0
    var heartRate: Double = 0
    var activeEnergy: Double = 0
    var statusMessage: String = "Tap to authorize"

    private let store = HKHealthStore()

    private var readTypes: Set<HKQuantityType> {
        [
            HKQuantityType(.stepCount),
            HKQuantityType(.heartRate),
            HKQuantityType(.activeEnergyBurned)
        ]
    }

    func requestAuthorization() async {
        guard HKHealthStore.isHealthDataAvailable() else {
            statusMessage = "HealthKit not available on this device"
            return
        }
        do {
            try await store.requestAuthorization(toShare: [], read: readTypes)
            statusMessage = "Authorized"
            await fetchAllMetrics()
        } catch {
            statusMessage = "Authorization failed: \(error.localizedDescription)"
        }
    }

    func fetchAllMetrics() async {
        async let s = fetchStatistic(.stepCount,
                                     unit: .count(),
                                     options: .cumulativeSum,
                                     picker: \.sumQuantity)
        async let h = fetchStatistic(.heartRate,
                                     unit: HKUnit(from: "count/min"),
                                     options: .discreteAverage,
                                     picker: \.averageQuantity)
        async let e = fetchStatistic(.activeEnergyBurned,
                                     unit: .kilocalorie(),
                                     options: .cumulativeSum,
                                     picker: \.sumQuantity)
        let (steps, hr, energy) = await (s, h, e)
        stepCount = Int(steps)
        heartRate = hr
        activeEnergy = energy
    }

    private func fetchStatistic(
        _ id: HKQuantityTypeIdentifier,
        unit: HKUnit,
        options: HKStatisticsOptions,
        picker: @escaping (HKStatistics) -> HKQuantity?
    ) async -> Double {
        let type = HKQuantityType(id)
        let now = Date()
        let start = Calendar.current.startOfDay(for: now)
        let predicate = HKQuery.predicateForSamples(withStart: start, end: now)

        return await withCheckedContinuation { continuation in
            let query = HKStatisticsQuery(
                quantityType: type,
                quantitySamplePredicate: predicate,
                options: options
            ) { _, statistics, _ in
                let value = statistics.flatMap(picker)?.doubleValue(for: unit) ?? 0
                continuation.resume(returning: value)
            }
            store.execute(query)
        }
    }
}

// MARK: - Views

struct HealthDashboardView: View {
    @State private var manager = HealthKitManager()

    var body: some View {
        NavigationStack {
            List {
                Section("Today's Activity") {
                    MetricRow(
                        label: "Steps",
                        value: manager.stepCount.formatted(),
                        icon: "figure.walk",
                        tint: .blue
                    )
                    MetricRow(
                        label: "Heart Rate",
                        value: String(format: "%.0f bpm", manager.heartRate),
                        icon: "heart.fill",
                        tint: .red
                    )
                    MetricRow(
                        label: "Active Energy",
                        value: String(format: "%.0f kcal", manager.activeEnergy),
                        icon: "flame.fill",
                        tint: .orange
                    )
                }
                Section("Authorization") {
                    Text(manager.statusMessage)
                        .font(.footnote)
                        .foregroundStyle(.secondary)
                }
            }
            .navigationTitle("Health")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        Task { await manager.fetchAllMetrics() }
                    } label: {
                        Label("Refresh", systemImage: "arrow.clockwise")
                    }
                }
            }
            .task {
                await manager.requestAuthorization()
            }
        }
    }
}

struct MetricRow: View {
    let label: String
    let value: String
    let icon: String
    let tint: Color

    var body: some View {
        HStack {
            Image(systemName: icon)
                .foregroundStyle(tint)
                .frame(width: 28)
                .accessibilityHidden(true)
            Text(label)
            Spacer()
            Text(value)
                .foregroundStyle(.secondary)
                .monospacedDigit()
        }
        .accessibilityElement(children: .combine)
        .accessibilityLabel("\(label): \(value)")
    }
}

// MARK: - Preview

#Preview {
    HealthDashboardView()
}

How it works

  1. Capability + Info.plist gate: HKHealthStore.isHealthDataAvailable() returns false on iPad and Mac Catalyst without HealthKit entitlements and on Simulator. Always guard on this before calling anything else to avoid a runtime crash.
  2. requestAuthorization(toShare:read:): The async/throws overload (available since iOS 15, recommended for iOS 17+) suspends the current task while the system sheet is presented. Passing an empty set for toShare means read-only — never request write access you don't need, or App Store review will flag it.
  3. async let concurrency: The three fetchStatistic calls inside fetchAllMetrics() are dispatched simultaneously. The await (s, h, e) tuple line waits for all three to complete before updating the @Observable properties, which triggers a single SwiftUI render pass.
  4. HKStatisticsQuery + withCheckedContinuation: HealthKit queries use a callback-based API. Wrapping them in withCheckedContinuation bridges them into Swift concurrency cleanly without Combine or a custom publisher. The picker closure chooses between sumQuantity() (steps, energy) and averageQuantity() (heart rate).
  5. @Observable manager: Annotating the class with @Observable (Swift 5.9 / iOS 17) means the view re-renders only when a property it actually reads changes. No @Published boilerplate required. The manager is stored as @State in the view so it has the same lifetime as the view hierarchy.

Variants

Live updates with HKObserverQuery

Use HKObserverQuery to receive a callback whenever new samples arrive in the Health store — useful for live workout screens or background delivery via enableBackgroundDelivery.

// Add inside HealthKitManager
private var observerQuery: HKObserverQuery?

func startObservingSteps() {
    let stepType = HKQuantityType(.stepCount)
    observerQuery = HKObserverQuery(
        sampleType: stepType,
        predicate: nil
    ) { [weak self] _, completionHandler, error in
        guard error == nil else { completionHandler(); return }
        Task { await self?.fetchAllMetrics() }
        completionHandler()   // required — signals HealthKit delivery is handled
    }
    if let q = observerQuery { store.execute(q) }

    // Enable background delivery (requires HealthKit background modes entitlement)
    store.enableBackgroundDelivery(
        for: stepType,
        frequency: .immediate
    ) { _, _ in }
}

func stopObservingSteps() {
    if let q = observerQuery { store.stop(q) }
    observerQuery = nil
}

Writing workouts with HKWorkoutBuilder

To log a workout (e.g., a run), pass HKQuantityType(.activeEnergyBurned) and HKQuantityType(.distanceWalkingRunning) in the toShare set, then use HKWorkoutBuilder with beginCollection(withStart:completion:) and endCollection(withEnd:completion:). Always pair write access with a clear user-facing action — the HealthKit review team checks this closely.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement HealthKit integration in SwiftUI for iOS 17+.
Use HKHealthStore, HKStatisticsQuery, and async/await authorization.
Read step count, heart rate, and active energy burned for today.
Wrap the @Observable manager in a NavigationStack dashboard view.
Make it accessible (VoiceOver labels on each metric row).
Add a #Preview with realistic sample data.

In Soarias, drop this prompt into the Build phase after your screens are scaffolded — Claude Code will wire up the HealthKit capability, generate the Info.plist keys, and produce a compilable manager class in one shot.

Related

FAQ

Does this work on iOS 16?

The @Observable macro and the async/throws overload of requestAuthorization require iOS 17. For iOS 16 support, replace @Observable with ObservableObject + @Published and wrap the authorization call in withCheckedThrowingContinuation using the completion-handler variant. Everything else — HKStatisticsQuery, HKObserverQuery, the query types used here — is available back to iOS 13.

How do I read workout routes or sleep data?

Add the relevant type to your readTypes set — e.g., HKObjectType.workoutType() for workouts or HKCategoryType(.sleepAnalysis) for sleep. Then use HKSampleQuery (for ordered samples) instead of HKStatisticsQuery, which only works with HKQuantityType. Wrap it the same way with withCheckedContinuation.

What's the UIKit equivalent?

HealthKit is framework-agnostic — the HKHealthStore, query, and authorization APIs are identical in UIKit. The only difference is bridging: in UIKit you'd typically deliver results back to the main thread with DispatchQueue.main.async and update a UILabel, whereas in SwiftUI the @Observable / .task pattern handles that for you.

Last reviewed: 2026-05-12 by the Soarias team.

```