
Healthkit
- 2.7k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
healthkit is a Swift skill for Apple HealthKit authorization, queries, writes, background delivery, and workouts.
About
healthkit guides Swift 6.3 and iOS 26+ apps through Apple HealthKit setup, authorization, and data access patterns. It covers enabling the HealthKit capability, Info.plist usage descriptions, background delivery sub-capability, and checking HKHealthStore.isHealthDataAvailable before any calls. Authorization requests only needed read and share types, with async HKSampleQueryDescriptor reads, HKStatisticsQueryDescriptor aggregates, and HKStatisticsCollectionQueryDescriptor time series for charts and long-running update streams. Writing uses HKQuantitySample saves your app created, while background delivery pairs enableBackgroundDelivery with HKObserverQuery completion handlers tested on device because Simulator lacks delivery. Workout sessions use HKWorkoutSession and HKLiveWorkoutBuilder with availability gates for older iOS releases and external heart rate sensors on iPhone. The skill documents HKQuantityTypeIdentifier tables, HKUnit mappings, cumulative versus discrete statistics options, common App Review mistakes, and a review checklist for permissions, threading, and background entitlements.
- Async query descriptors for samples, statistics, and chart intervals.
- Authorization scoped to required types with read-denial privacy behavior.
- Background delivery paired with observer queries and completion handlers.
- HKWorkoutSession and HKLiveWorkoutBuilder patterns with API gates.
- Review checklist for entitlements, units, and statistics option matching.
Healthkit by the numbers
- 2,733 all-time installs (skills.sh)
- +111 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #68 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
healthkit capabilities & compatibility
- Capabilities
- healthkit availability and entitlement setup · async sample and statistics query descriptors · quantity sample writes and deletion rules · background delivery with observer queries · live workout session and builder lifecycle · hkunit reference and common mistake guards
- Use cases
- api development · frontend
- Platforms
- macOS
- Runs
- Runs locally
- Pricing
- Free
What healthkit says it does
Request only the types your app genuinely needs.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill healthkitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.7k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do I read steps, heart rate, and workouts from Apple Health with correct authorization and queries?
Read, write, and query Apple Health data with HealthKit authorization, statistics queries, background delivery, and live workouts in Swift.
Who is it for?
iOS developers integrating fitness metrics, vitals charts, and live workout tracking.
Skip if: Skip for Android health APIs, server-only analytics, or non-Apple wearable SDKs.
When should I use this skill?
User mentions HealthKit, HKHealthStore, step counts, workout sessions, or background health delivery.
What you get
Configured HealthKit access with async queries, writes, background updates, and workout sessions.
- HealthKit setup corrections
- Authorization and availability checklist
Files
HealthKit
Read and write health and fitness data from the Apple Health store. Covers authorization, queries, writing samples, background delivery, and workout sessions. Targets Swift 6.3 / iOS 26+.
Contents
- Setup and Availability
- Authorization
- Reading Data: Sample Queries
- Reading Data: Statistics Queries
- Reading Data: Statistics Collection Queries
- Writing Data
- Background Delivery
- Workout Sessions
- Common Data Types
- HKUnit Reference
- Common Mistakes
- Review Checklist
- References
Setup and Availability
Project Configuration
1. Enable the HealthKit capability in Xcode (adds the entitlement) 2. Add NSHealthShareUsageDescription (read) and NSHealthUpdateUsageDescription (write) to Info.plist 3. For background delivery, enable the "Background Delivery" sub-capability
Availability Check
Always check availability before calling other HealthKit APIs. Health data is available on iOS, watchOS, visionOS, iPadOS 17+, and iOS apps running on Vision Pro. It is unavailable on iPadOS 16 or earlier and may be restricted by managed device policy.
import HealthKit
guard HKHealthStore.isHealthDataAvailable() else {
// Health data is unavailable or restricted on this device.
return
}
let healthStore = HKHealthStore()Create a single HKHealthStore instance and reuse it throughout your app. It is thread-safe. If HealthKit is optional, review Xcode's generated UIRequiredDeviceCapabilities healthkit entry so unsupported devices are not excluded unintentionally.
Authorization
Request only the types your app genuinely needs. App Review rejects apps that over-request.
func requestAuthorization() async throws {
let typesToShare: Set<HKSampleType> = [
HKQuantityType(.stepCount),
HKQuantityType(.activeEnergyBurned)
]
let typesToRead: Set<HKObjectType> = [
HKQuantityType(.stepCount),
HKQuantityType(.heartRate),
HKQuantityType(.activeEnergyBurned),
HKCharacteristicType(.dateOfBirth)
]
try await healthStore.requestAuthorization(
toShare: typesToShare,
read: typesToRead
)
}Checking Authorization Status
authorizationStatus(for:) reports write/share authorization. HealthKit does not reveal whether read permission was granted or denied. If the user denies read access, queries return only samples your app successfully saved, which may look like empty or partial data.
let status = healthStore.authorizationStatus(
for: HKQuantityType(.stepCount)
)
switch status {
case .notDetermined:
// Haven't requested yet -- safe to call requestAuthorization
break
case .sharingAuthorized:
// User granted write access
break
case .sharingDenied:
// User denied write access (read denial is indistinguishable from "no data")
break
@unknown default:
break
}Reading Data: Sample Queries
Use HKSampleQueryDescriptor (async/await) for one-shot reads. Prefer descriptors over the older callback-based HKSampleQuery.
func fetchRecentHeartRates() async throws -> [HKQuantitySample] {
let heartRateType = HKQuantityType(.heartRate)
let descriptor = HKSampleQueryDescriptor(
predicates: [.quantitySample(type: heartRateType)],
sortDescriptors: [SortDescriptor(\.endDate, order: .reverse)],
limit: 20
)
let results = try await descriptor.result(for: healthStore)
return results
}
// Extracting values from samples:
for sample in results {
let bpm = sample.quantity.doubleValue(
for: HKUnit.count().unitDivided(by: .minute())
)
print("\(bpm) bpm at \(sample.endDate)")
}Reading Data: Statistics Queries
Use HKStatisticsQueryDescriptor for aggregated single-value stats (sum, average, min, max).
func fetchTodayStepCount() async throws -> Double? {
let calendar = Calendar.current
let startOfDay = calendar.startOfDay(for: Date())
let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)!
let predicate = HKQuery.predicateForSamples(
withStart: startOfDay, end: endOfDay
)
let stepType = HKQuantityType(.stepCount)
let samplePredicate = HKSamplePredicate.quantitySample(
type: stepType, predicate: predicate
)
let query = HKStatisticsQueryDescriptor(
predicate: samplePredicate,
options: .cumulativeSum
)
let result = try await query.result(for: healthStore)
return result?.sumQuantity()?.doubleValue(for: .count())
}Options by data type:
- Cumulative types (steps, calories):
.cumulativeSum - Discrete types (heart rate, weight):
.discreteAverage,.discreteMin,.discreteMax
Reading Data: Statistics Collection Queries
Use HKStatisticsCollectionQueryDescriptor for time-series data grouped into intervals -- ideal for charts.
func fetchDailySteps(forLast days: Int) async throws -> [(date: Date, steps: Double)] {
let calendar = Calendar.current
let endDate = calendar.startOfDay(
for: calendar.date(byAdding: .day, value: 1, to: Date())!
)
let startDate = calendar.date(byAdding: .day, value: -days, to: endDate)!
let predicate = HKQuery.predicateForSamples(
withStart: startDate, end: endDate
)
let stepType = HKQuantityType(.stepCount)
let samplePredicate = HKSamplePredicate.quantitySample(
type: stepType, predicate: predicate
)
let query = HKStatisticsCollectionQueryDescriptor(
predicate: samplePredicate,
options: .cumulativeSum,
anchorDate: endDate,
intervalComponents: DateComponents(day: 1)
)
let collection = try await query.result(for: healthStore)
var dailySteps: [(date: Date, steps: Double)] = []
collection.statisticsCollection.enumerateStatistics(
from: startDate, to: endDate
) { statistics, _ in
let steps = statistics.sumQuantity()?
.doubleValue(for: .count()) ?? 0
dailySteps.append((date: statistics.startDate, steps: steps))
}
return dailySteps
}Long-Running Collection Query
Use results(for:) (plural) to get an AsyncSequence that emits updates as new data arrives:
let updateStream = query.results(for: healthStore)
Task {
for try await result in updateStream {
// result.statisticsCollection contains updated data
}
}Writing Data
Create HKQuantitySample objects and save them to the store.
func saveSteps(count: Double, start: Date, end: Date) async throws {
let stepType = HKQuantityType(.stepCount)
let quantity = HKQuantity(unit: .count(), doubleValue: count)
let sample = HKQuantitySample(
type: stepType,
quantity: quantity,
start: start,
end: end
)
try await healthStore.save(sample)
}
Your app can only delete samples it created. Samples from other apps or Apple Watch are read-only.
Background Delivery
Register for background updates so your app is launched when new data arrives. Requires the background delivery entitlement.
func enableStepCountBackgroundDelivery() async throws {
let stepType = HKQuantityType(.stepCount)
try await healthStore.enableBackgroundDelivery(
for: stepType,
frequency: .hourly
)
}Pair with an `HKObserverQuery` to handle notifications. Always call the completion handler:
let observerQuery = HKObserverQuery(
sampleType: HKQuantityType(.stepCount),
predicate: nil
) { query, completionHandler, error in
defer { completionHandler() } // Must call to signal done
guard error == nil else { return }
// Fetch new data, update UI, etc.
}
healthStore.execute(observerQuery)Frequencies: .immediate, .hourly, .daily, .weekly
Set up observer queries as soon as the app launches, then call enableBackgroundDelivery once for the same sample type. The system persists the registration, wakes the app at most once per requested frequency, and enforces tighter caps for some types such as hourly step-count delivery on iOS. Background delivery is not supported on Simulator; test it on device.
Workout Sessions
Use HKWorkoutSession and HKLiveWorkoutBuilder to track live workouts. HKWorkoutSession is available on iOS/iPadOS 17+, visionOS 1+, and watchOS 2+. HKLiveWorkoutBuilder is available on iOS/iPadOS 26+ and watchOS 5+, so gate live-builder code if supporting older iOS/iPadOS releases.
On iPhone and iPad, live heart-rate collection requires a paired external heart rate sensor. Apple Watch sessions can collect high-frequency heart-rate data. For locked iPhone workouts, plan for the system's workout-data access flow before showing health metrics on the Lock Screen.
func startWorkout() async throws {
let configuration = HKWorkoutConfiguration()
configuration.activityType = .running
configuration.locationType = .outdoor
let session = try HKWorkoutSession(
healthStore: healthStore,
configuration: configuration
)
session.delegate = self
let builder = session.associatedWorkoutBuilder()
builder.dataSource = HKLiveWorkoutDataSource(
healthStore: healthStore,
workoutConfiguration: configuration
)
session.startActivity(with: Date())
try await builder.beginCollection(at: Date())
}
func endWorkout(
session: HKWorkoutSession,
builder: HKLiveWorkoutBuilder
) async throws {
session.end()
try await builder.endCollection(at: Date())
try await builder.finishWorkout()
}For full workout lifecycle management including pause/resume, delegate handling, and multi-device mirroring, see references/healthkit-patterns.md.
Common Data Types
HKQuantityTypeIdentifier
| Identifier | Category | Unit |
|---|---|---|
.stepCount | Fitness | .count() |
.distanceWalkingRunning | Fitness | .meter() |
.activeEnergyBurned | Fitness | .kilocalorie() |
.basalEnergyBurned | Fitness | .kilocalorie() |
.heartRate | Vitals | .count()/.minute() |
.restingHeartRate | Vitals | .count()/.minute() |
.oxygenSaturation | Vitals | .percent() |
.bodyMass | Body | .gramUnit(with: .kilo) |
.bodyMassIndex | Body | .count() |
.height | Body | .meter() |
.bodyFatPercentage | Body | .percent() |
.bloodGlucose | Lab | .gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)) |
HKCategoryTypeIdentifier
Common category types: .sleepAnalysis, .mindfulSession, .appleStandHour
HKCharacteristicType
Read-only user characteristics include .dateOfBirth, .biologicalSex, .bloodType, .fitzpatrickSkinType, .wheelchairUse, and .activityMoveMode.
HKUnit Reference
// Basic units
HKUnit.count() // Steps, counts
HKUnit.meter() // Distance
HKUnit.mile() // Distance (imperial)
HKUnit.kilocalorie() // Energy
HKUnit.joule(with: .kilo) // Energy (SI)
HKUnit.gramUnit(with: .kilo) // Mass (kg)
HKUnit.pound() // Mass (imperial)
HKUnit.percent() // Percentage
// Compound units
HKUnit.count().unitDivided(by: .minute()) // Heart rate (bpm)
HKUnit.meter().unitDivided(by: .second()) // Speed (m/s)
// Prefixed units
HKUnit.gramUnit(with: .milli) // Milligrams
HKUnit.literUnit(with: .deci) // DecilitersCommon Mistakes
1. Over-requesting data types. Request only the read/write types the feature actually uses; broad HealthKit permission sheets are an App Review risk. 2. Treating read authorization like write authorization. You can check .sharingAuthorized before saving, but read denial is privacy-protected and looks like app-owned-only, empty, or partial results. 3. Skipping `isHealthDataAvailable()`. Check before HealthKit access and handle unavailable or restricted stores without crashing. 4. Using callback queries for new async code. Prefer async descriptors for one-shot reads and statistics, and keep broad queries off the main actor. 5. Forgetting observer completion handlers. Always call the handler; missed completions can delay or stop future background deliveries. 6. Assuming `.immediate` means immediate. Background delivery is capped by the system and must be tested on device. 7. Using cumulative stats for discrete values. Match statistics options to the data type: cumulative sums for steps/energy, discrete average/min/max for heart rate, weight, and similar samples.
Review Checklist
- [ ]
HKHealthStore.isHealthDataAvailable()checked before any HealthKit access - [ ] Only necessary data types requested in authorization
- [ ]
Info.plistincludesNSHealthShareUsageDescriptionand/orNSHealthUpdateUsageDescription - [ ] HealthKit capability enabled in Xcode project
- [ ] Write authorization checked before saving; read denial handled as partial
or empty query results
- [ ] Single
HKHealthStoreinstance reused (not created per query) - [ ] Async query descriptors used instead of callback-based queries
- [ ] Heavy queries not blocking main thread
- [ ] Statistics options match data type (cumulative vs. discrete)
- [ ] Background delivery paired with app-launch
HKObserverQuerysetup and
completionHandler called
- [ ] Background delivery entitlement enabled if using
enableBackgroundDelivery - [ ] Background delivery tested on device and frequency caps considered
- [ ] Workout sessions properly ended and builder finalized
- [ ] Workout API availability and live heart-rate sensor requirements handled
- [ ] Write operations only for sample types the app created
References
- Extended patterns (workouts, anchored queries, SwiftUI integration): references/healthkit-patterns.md
- HealthKit framework
- HKHealthStore
- HKSampleQueryDescriptor
- HKStatisticsQueryDescriptor
- HKStatisticsCollectionQueryDescriptor
- HKWorkoutSession
- HKLiveWorkoutBuilder
- Setting up HealthKit
- Authorizing access to health data
- Configuring HealthKit access
{
"skill_name": "healthkit",
"evals": [
{
"id": 0,
"prompt": "Review this HealthKit setup plan for an iOS 26/iPadOS app: the team says HealthKit never works on iPad, creates HKHealthStore before any availability check, treats authorizationStatus as proof that read permission was denied, and assumes denied reads always return empty arrays. Give concise corrected guidance.",
"expected_output": "A correction-focused review that explains current platform availability, availability checks, write-vs-read authorization privacy, and partial app-owned results after read denial.",
"files": [],
"assertions": [
"States that HealthKit data is available on iPadOS 17 or later, not categorically unsupported on iPad.",
"Requires HKHealthStore.isHealthDataAvailable() before using other HealthKit APIs.",
"Explains that authorizationStatus(for:) reports write/share authorization, not whether read access was granted.",
"Explains that denied read access can return only samples the app saved, so results may be empty or partial rather than a reliable denial signal.",
"Mentions required HealthKit capability and NSHealthShareUsageDescription or NSHealthUpdateUsageDescription as appropriate."
]
},
{
"id": 1,
"prompt": "Review this background step-count sync plan: enableBackgroundDelivery(.immediate) is called after the first dashboard screen appears, the HKObserverQuery is created only when the screen is visible, the completion handler is skipped on errors, and QA plans to validate the behavior only in Simulator. What should change?",
"expected_output": "A review that fixes HealthKit background delivery setup, launch-time observer registration, completion handling, device testing, entitlement, and frequency expectations.",
"files": [],
"assertions": [
"Requires the HealthKit Background Delivery capability or com.apple.developer.healthkit.background-delivery entitlement.",
"Says observer queries should be set up as the app launches, not only when a screen appears.",
"Pairs enableBackgroundDelivery with an HKObserverQuery for the same sample type.",
"Requires calling the observer completion handler even when processing fails.",
"States that Simulator is not a valid background-delivery test target.",
"Explains that requested frequency is a maximum and some types such as iOS step count may be capped hourly."
]
},
{
"id": 2,
"prompt": "A team is building an iOS 26 workout recorder with HKWorkoutSession and HKLiveWorkoutBuilder. They also want the same code path to support iOS 17 devices, assume iPhone has live heart-rate samples like Apple Watch, and mirror Apple Watch workouts to iPhone whenever possible. Review the plan and identify HealthKit pitfalls.",
"expected_output": "A workout-session review that distinguishes HKWorkoutSession and HKLiveWorkoutBuilder availability, handles live heart-rate collection on iPhone/iPad, and outlines correct mirroring setup.",
"files": [],
"assertions": [
"Distinguishes HKWorkoutSession availability from HKLiveWorkoutBuilder availability.",
"States that HKLiveWorkoutBuilder requires iOS/iPadOS 26 or watchOS 5 and should be availability-gated for older iOS/iPadOS targets.",
"States that iPhone and iPad need a paired external heart-rate sensor for live heart-rate collection.",
"Mentions that Apple Watch can collect high-frequency heart-rate samples during workouts.",
"For mirroring, starts mirroring from the watchOS session and assigns workoutSessionMirroringStartHandler early on the iOS app.",
"Mentions lock-screen or locked-device workout data access considerations when showing live iPhone metrics."
]
}
]
}
HealthKit Extended Patterns
Overflow reference for the healthkit skill. Contains advanced patterns that exceed the main skill file's scope.
Contents
- Workout Session Lifecycle
- Platform and Authorization Edge Cases
- Background Delivery Details
- Anchored Object Queries
- Predicate-Based Filtering
- Statistics Collection for Charts
- HealthKit + SwiftUI Integration
- Characteristic Types
Workout Session Lifecycle
HKWorkoutSession is available on iOS/iPadOS 17+, visionOS 1+, and watchOS 2+. HKLiveWorkoutBuilder is available on iOS/iPadOS 26+ and watchOS 5+. When supporting iOS or iPadOS earlier than 26, gate live-builder code and use a non-live workout save path where appropriate.
iPhone and iPad do not provide built-in live heart-rate samples. They require a paired external heart-rate sensor for live heart-rate collection, while Apple Watch workout sessions collect high-frequency heart-rate samples. iPhone also often locks during workouts; if the app shows health metrics on the Lock Screen, design around the system's workout-data access prompt and Live Activity surface.
Full Workout Manager
import HealthKit
@Observable
@MainActor
final class WorkoutManager: NSObject {
let healthStore = HKHealthStore()
private var session: HKWorkoutSession?
private var builder: HKLiveWorkoutBuilder?
var heartRate: Double = 0
var activeCalories: Double = 0
var distance: Double = 0
var elapsedTime: TimeInterval = 0
var isActive = false
func startWorkout(activityType: HKWorkoutActivityType) async throws {
let configuration = HKWorkoutConfiguration()
configuration.activityType = activityType
configuration.locationType = .outdoor
let session = try HKWorkoutSession(
healthStore: healthStore,
configuration: configuration
)
let builder = session.associatedWorkoutBuilder()
builder.dataSource = HKLiveWorkoutDataSource(
healthStore: healthStore,
workoutConfiguration: configuration
)
session.delegate = self
builder.delegate = self
self.session = session
self.builder = builder
session.startActivity(with: Date())
try await builder.beginCollection(at: Date())
isActive = true
}
func pause() {
session?.pause()
}
func resume() {
session?.resume()
}
func end() async throws {
guard let session, let builder else { return }
session.end()
try await builder.endCollection(at: Date())
try await builder.finishWorkout()
isActive = false
self.session = nil
self.builder = nil
}
}
// MARK: - HKWorkoutSessionDelegate
extension WorkoutManager: HKWorkoutSessionDelegate {
nonisolated func workoutSession(
_ workoutSession: HKWorkoutSession,
didChangeTo toState: HKWorkoutSessionState,
from fromState: HKWorkoutSessionState,
date: Date
) {
Task { @MainActor in
switch toState {
case .running:
isActive = true
case .paused:
isActive = false
case .ended, .stopped:
isActive = false
default:
break
}
}
}
nonisolated func workoutSession(
_ workoutSession: HKWorkoutSession,
didFailWithError error: Error
) {
Task { @MainActor in
print("Workout session failed: \(error)")
isActive = false
}
}
}
// MARK: - HKLiveWorkoutBuilderDelegate
extension WorkoutManager: HKLiveWorkoutBuilderDelegate {
nonisolated func workoutBuilderDidCollectEvent(
_ workoutBuilder: HKLiveWorkoutBuilder
) {
// Handle workout events (pause, resume, lap, etc.)
}
nonisolated func workoutBuilder(
_ workoutBuilder: HKLiveWorkoutBuilder,
didCollectDataOf collectedTypes: Set<HKSampleType>
) {
Task { @MainActor in
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else { continue }
let statistics = workoutBuilder.statistics(for: quantityType)
switch quantityType {
case HKQuantityType(.heartRate):
heartRate = statistics?.mostRecentQuantity()?
.doubleValue(for: HKUnit.count().unitDivided(by: .minute())) ?? 0
case HKQuantityType(.activeEnergyBurned):
activeCalories = statistics?.sumQuantity()?
.doubleValue(for: .kilocalorie()) ?? 0
case HKQuantityType(.distanceWalkingRunning):
distance = statistics?.sumQuantity()?
.doubleValue(for: .meter()) ?? 0
default:
break
}
}
elapsedTime = workoutBuilder.elapsedTime
}
}
}Multi-Device Mirroring (watchOS + iOS)
Start mirroring from the primary watchOS session. In the iOS companion app, assign workoutSessionMirroringStartHandler as the app launches so it can receive mirrored sessions even when launched in the background. The handler runs on an arbitrary background queue and may be called more than once if the devices disconnect and reconnect during a workout.
// On watchOS: start mirroring to companion iPhone
func startMirroring() async throws {
try await session?.startMirroringToCompanionDevice()
}
// On iOS: receive the mirrored session
func setupMirroredSessionHandler() {
healthStore.workoutSessionMirroringStartHandler = { mirroredSession in
// mirroredSession is an HKWorkoutSession with type == .mirrored
mirroredSession.delegate = self
let builder = mirroredSession.associatedWorkoutBuilder()
builder.delegate = self
}
}
// Send data between devices
func sendDataToRemote(_ data: Data) async throws {
try await session?.sendToRemoteWorkoutSession(data: data)
}Platform and Authorization Edge Cases
- Call
HKHealthStore.isHealthDataAvailable()before any other HealthKit API.
Current Apple docs say Health data is available on iOS, watchOS, visionOS, iPadOS 17+, and iOS apps running on Vision Pro. It is unavailable on iPadOS 16 or earlier and may be restricted by managed device policy.
- Enabling the HealthKit capability for an iOS app can add
healthkitto
UIRequiredDeviceCapabilities, preventing installation on unsupported devices. Remove that entry only when HealthKit is optional and the app has a useful non-HealthKit mode.
authorizationStatus(for:)is for write/share authorization. HealthKit does
not reveal whether read access was granted or denied. If read access is denied, queries return samples your app saved successfully, which can look like partial or empty data.
- People can change HealthKit permissions later in Settings or the Health app.
Refresh permission-sensitive UI and write paths instead of assuming the original authorization outcome still applies.
- In Vision Pro Guest User sessions, previously authorized data may be readable,
but new authorization and writes can fail. Treat HealthKit writes as best-effort unless the user explicitly initiated a save action that needs an explanation.
Background Delivery Details
Background delivery requires the HealthKit Background Delivery capability (com.apple.developer.healthkit.background-delivery on current Apple docs) and an executed HKObserverQuery for the same sample type. Set up observer queries when the app launches, then call enableBackgroundDelivery once; the system persists the registration.
func configureHealthKitBackgroundDelivery() async throws {
let stepType = HKQuantityType(.stepCount)
let query = HKObserverQuery(
sampleType: stepType,
predicate: nil
) { _, completionHandler, error in
defer { completionHandler() }
guard error == nil else { return }
Task {
// Run an anchored query or statistics refresh here.
}
}
healthStore.execute(query)
try await healthStore.enableBackgroundDelivery(
for: stepType,
frequency: .hourly
)
}Treat HKUpdateFrequency as a maximum delivery rate, not a guarantee. Some types have tighter system caps; for example, iOS step-count background delivery is capped at hourly even if .immediate is requested. Background server queries are not supported on Simulator, so validate delivery on real hardware. If the observer completion handler is not called, HealthKit backs off and can stop sending background updates after repeated failures.
Anchored Object Queries
Use HKAnchoredObjectQuery for incremental updates -- only fetches samples added or deleted since the last anchor.
@Observable
final class StepTracker {
private let healthStore = HKHealthStore()
private var anchor: HKQueryAnchor?
private var observerQuery: HKObserverQuery?
var totalSteps: Double = 0
func startMonitoring() {
let stepType = HKQuantityType(.stepCount)
// Initial fetch + ongoing updates
let anchoredQuery = HKAnchoredObjectQuery(
type: stepType,
predicate: nil,
anchor: anchor,
limit: HKObjectQueryNoLimit
) { [weak self] query, added, deleted, newAnchor, error in
guard let self else { return }
self.anchor = newAnchor
self.processNewSamples(added ?? [])
}
// Enable updates handler for real-time monitoring
anchoredQuery.updateHandler = { [weak self] query, added, deleted, newAnchor, error in
guard let self else { return }
self.anchor = newAnchor
self.processNewSamples(added ?? [])
}
healthStore.execute(anchoredQuery)
}
private func processNewSamples(_ samples: [HKSample]) {
for sample in samples {
guard let quantitySample = sample as? HKQuantitySample else { continue }
let steps = quantitySample.quantity.doubleValue(for: .count())
totalSteps += steps
}
}
}Anchored Query with Async Descriptor
import HealthKit
let stepType = HKQuantityType(.stepCount)
let descriptor = HKAnchoredObjectQueryDescriptor(
predicates: [.quantitySample(type: stepType)],
anchor: savedAnchor
)
// One-shot
let result = try await descriptor.result(for: healthStore)
let newSamples = result.addedSamples
let deletedObjects = result.deletedObjects
let newAnchor = result.newAnchor
// Long-running with updates
for try await result in descriptor.results(for: healthStore) {
// Process result.addedSamples and result.deletedObjects
}Predicate-Based Filtering
Time-Based Predicates
// Samples from today
let today = Calendar.current.startOfDay(for: Date())
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)!
let todayPredicate = HKQuery.predicateForSamples(
withStart: today, end: tomorrow
)
// Samples from the last 7 days
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
let weekPredicate = HKQuery.predicateForSamples(
withStart: oneWeekAgo, end: Date()
)
// Strict: sample must be entirely within the range
let strictPredicate = HKQuery.predicateForSamples(
withStart: today, end: tomorrow,
options: .strictStartDate
)Source-Based Predicates
// Only samples from the current app
let sourcePredicate = HKQuery.predicateForObjects(
from: HKSource.default()
)
// Only samples from Apple Watch
let devicePredicate = HKQuery.predicateForObjects(
withDeviceProperty: HKDevicePropertyKeyModel,
allowedValues: ["Watch"]
)Compound Predicates
let todayFromWatch = NSCompoundPredicate(
andPredicateWithSubpredicates: [todayPredicate, devicePredicate]
)
let descriptor = HKSampleQueryDescriptor(
predicates: [.quantitySample(type: stepType, predicate: todayFromWatch)],
sortDescriptors: [SortDescriptor(\.endDate, order: .reverse)]
)Statistics Collection for Charts
Weekly Step Count Chart Data
struct DailyStepData: Identifiable {
let id = UUID()
let date: Date
let steps: Double
}
func fetchWeeklyStepData() async throws -> [DailyStepData] {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let endDate = calendar.date(byAdding: .day, value: 1, to: today)!
let startDate = calendar.date(byAdding: .day, value: -7, to: endDate)!
let stepType = HKQuantityType(.stepCount)
let predicate = HKQuery.predicateForSamples(
withStart: startDate, end: endDate
)
let samplePredicate = HKSamplePredicate.quantitySample(
type: stepType, predicate: predicate
)
let query = HKStatisticsCollectionQueryDescriptor(
predicate: samplePredicate,
options: .cumulativeSum,
anchorDate: endDate,
intervalComponents: DateComponents(day: 1)
)
let collection = try await query.result(for: healthStore)
var data: [DailyStepData] = []
collection.statisticsCollection.enumerateStatistics(
from: startDate, to: endDate
) { statistics, _ in
let steps = statistics.sumQuantity()?
.doubleValue(for: .count()) ?? 0
data.append(DailyStepData(date: statistics.startDate, steps: steps))
}
return data
}Hourly Heart Rate Averages
func fetchHourlyHeartRate(for date: Date) async throws -> [(hour: Date, bpm: Double)] {
let calendar = Calendar.current
let startOfDay = calendar.startOfDay(for: date)
let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)!
let heartRateType = HKQuantityType(.heartRate)
let predicate = HKQuery.predicateForSamples(
withStart: startOfDay, end: endOfDay
)
let samplePredicate = HKSamplePredicate.quantitySample(
type: heartRateType, predicate: predicate
)
let query = HKStatisticsCollectionQueryDescriptor(
predicate: samplePredicate,
options: .discreteAverage,
anchorDate: endOfDay,
intervalComponents: DateComponents(hour: 1)
)
let collection = try await query.result(for: healthStore)
let unit = HKUnit.count().unitDivided(by: .minute())
var hourlyData: [(hour: Date, bpm: Double)] = []
collection.statisticsCollection.enumerateStatistics(
from: startOfDay, to: endOfDay
) { statistics, _ in
if let avg = statistics.averageQuantity()?.doubleValue(for: unit) {
hourlyData.append((hour: statistics.startDate, bpm: avg))
}
}
return hourlyData
}HealthKit + SwiftUI Integration
HealthKit Manager with @Observable
import HealthKit
import SwiftUI
@Observable
@MainActor
final class HealthManager {
let healthStore = HKHealthStore()
var isAuthorized = false
var todaySteps: Double = 0
var recentHeartRate: Double = 0
var isAvailable: Bool {
HKHealthStore.isHealthDataAvailable()
}
func requestAuthorization() async throws {
let typesToRead: Set<HKObjectType> = [
HKQuantityType(.stepCount),
HKQuantityType(.heartRate)
]
try await healthStore.requestAuthorization(
toShare: [],
read: typesToRead
)
isAuthorized = true
}
func refreshData() async {
async let steps = fetchTodaySteps()
async let heartRate = fetchLatestHeartRate()
todaySteps = (try? await steps) ?? 0
recentHeartRate = (try? await heartRate) ?? 0
}
private func fetchTodaySteps() async throws -> Double {
let calendar = Calendar.current
let startOfDay = calendar.startOfDay(for: Date())
let predicate = HKQuery.predicateForSamples(
withStart: startOfDay, end: Date()
)
let stepType = HKQuantityType(.stepCount)
let samplePredicate = HKSamplePredicate.quantitySample(
type: stepType, predicate: predicate
)
let query = HKStatisticsQueryDescriptor(
predicate: samplePredicate, options: .cumulativeSum
)
return try await query.result(for: healthStore)?
.sumQuantity()?.doubleValue(for: .count()) ?? 0
}
private func fetchLatestHeartRate() async throws -> Double {
let heartRateType = HKQuantityType(.heartRate)
let descriptor = HKSampleQueryDescriptor(
predicates: [.quantitySample(type: heartRateType)],
sortDescriptors: [SortDescriptor(\.endDate, order: .reverse)],
limit: 1
)
let results = try await descriptor.result(for: healthStore)
let unit = HKUnit.count().unitDivided(by: .minute())
return results.first?.quantity.doubleValue(for: unit) ?? 0
}
}SwiftUI View with HealthKit
struct HealthDashboardView: View {
@Environment(HealthManager.self) private var healthManager
var body: some View {
NavigationStack {
Group {
if !healthManager.isAvailable {
ContentUnavailableView(
"HealthKit Unavailable",
systemImage: "heart.slash",
description: Text("This device does not support HealthKit.")
)
} else if !healthManager.isAuthorized {
authorizationPrompt
} else {
healthDataView
}
}
.navigationTitle("Health")
}
}
private var authorizationPrompt: some View {
ContentUnavailableView {
Label("Health Access", systemImage: "heart.text.square")
} description: {
Text("Grant access to view your health data.")
} actions: {
Button("Authorize") {
Task {
try? await healthManager.requestAuthorization()
}
}
.buttonStyle(.borderedProminent)
}
}
private var healthDataView: some View {
List {
Section("Today") {
LabeledContent("Steps") {
Text(healthManager.todaySteps, format: .number.precision(.fractionLength(0)))
}
LabeledContent("Heart Rate") {
Text("\(Int(healthManager.recentHeartRate)) bpm")
}
}
}
.task {
await healthManager.refreshData()
}
.refreshable {
await healthManager.refreshData()
}
}
}App Entry Point Wiring
@main
struct MyHealthApp: App {
@State private var healthManager = HealthManager()
var body: some Scene {
WindowGroup {
HealthDashboardView()
.environment(healthManager)
}
}
}Characteristic Types
Characteristic types are read-only values set by the user in the Health app. They do not require sample queries.
func readCharacteristics() throws {
// Date of birth
let dobComponents = try healthStore.dateOfBirthComponents()
let calendar = Calendar.current
if let dob = calendar.date(from: dobComponents) {
let age = calendar.dateComponents([.year], from: dob, to: Date()).year ?? 0
print("Age: \(age)")
}
// Biological sex
let biologicalSex = try healthStore.biologicalSex().biologicalSex
switch biologicalSex {
case .female: print("Female")
case .male: print("Male")
case .other: print("Other")
case .notSet: print("Not set")
@unknown default: break
}
// Blood type
let bloodType = try healthStore.bloodType().bloodType
switch bloodType {
case .aPositive: print("A+")
case .aNegative: print("A-")
case .bPositive: print("B+")
case .bNegative: print("B-")
case .abPositive: print("AB+")
case .abNegative: print("AB-")
case .oPositive: print("O+")
case .oNegative: print("O-")
case .notSet: print("Not set")
@unknown default: break
}
// Fitzpatrick skin type
let skinType = try healthStore.fitzpatrickSkinType().skinType
print("Skin type: \(skinType.rawValue)")
// Wheelchair use
let wheelchair = try healthStore.wheelchairUse().wheelchairUse
print("Wheelchair: \(wheelchair == .yes)")
}Important: These throw an error if the value is not set, so always use try and handle the HKError.errorNoData case.
do {
let dob = try healthStore.dateOfBirthComponents()
// Use dob
} catch let error as HKError where error.code == .errorNoData {
// User hasn't set date of birth in Health app
} catch {
// Other error
}Related skills
How it compares
Use healthkit over generic Apple docs summaries when you need correction-focused guidance on HealthKit privacy semantics and iPad availability.
FAQ
Can I detect if read permission was denied?
No. Read denial is privacy-protected and looks like empty or partial query results.
Which statistics option fits step counts?
Use cumulativeSum for steps and active energy; discrete average, min, or max for heart rate.
Does background delivery work in Simulator?
No. Test enableBackgroundDelivery and observer queries on a physical device.
Is Healthkit safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.