HealthKit Integration for Health Data Access in iOS
HealthKit is not just API for reading Apple Watch data. Central health storage for iOS with strict data schema, granular permissions per data type and App Store policy that violates lead to rejection. Integration takes more time than seems: not from API complexity, but from permission edge-cases and usage rules quantity.
Permissions and App Store Review
Apple checks HealthKit integration manually on review. Main rejection reasons:
- App requests data types it doesn't use (
HKObjectTypemust match real functionality) - No
NSHealthShareUsageDescription/NSHealthUpdateUsageDescriptioninInfo.plist— banality crash on first request - App requests workout write permissions but isn't fitness app — rejection per Guideline 5.1.1 (Privacy)
HealthKit permission peculiarity: user can deny specific type, but app never learns explicitly. HKHealthStore.authorizationStatus(for:) returns .notDetermined on both deny and "not asked yet". This privacy protection — can't infer permission state whether data exists.
Practical consequence: can't show alert "you denied step access". Must silently try read data, if array empty — show neutral message "data unavailable" with "Open Health" button.
Reading Data: HKSampleQuery vs HKStatisticsQuery
HKSampleQuery returns raw samples — each pulse measurement, each glucose record, each step from each source. Active user accumulates tens thousands records yearly. Query without limit and sort — OutOfMemory or long wait.
let query = HKSampleQuery(
sampleType: HKQuantityType(.heartRate),
predicate: HKQuery.predicateForSamples(
withStart: startDate,
end: endDate,
options: .strictStartDate
),
limit: 1000,
sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)]
) { _, samples, error in
guard let samples = samples as? [HKQuantitySample] else { return }
let bpmValues = samples.map {
$0.quantity.doubleValue(for: .init(from: "count/min"))
}
// processing
}
healthStore.execute(query)
For aggregated data — HKStatisticsQuery or HKStatisticsCollectionQuery. Latter allows get statistics by intervals (day, week) per period — steps each day of month in one request:
let interval = DateComponents(day: 1)
let query = HKStatisticsCollectionQuery(
quantityType: HKQuantityType(.stepCount),
quantitySamplePredicate: nil,
options: .cumulativeSum,
anchorDate: Calendar.current.startOfDay(for: Date()),
intervalComponents: interval
)
query.initialResultsHandler = { _, results, _ in
results?.enumerateStatistics(from: startDate, to: endDate) { stat, _ in
let steps = stat.sumQuantity()?.doubleValue(for: .count()) ?? 0
}
}
HKAnchoredObjectQuery — for background updates: app gets only delta since last query. Use for syncing new workouts to server.
Recording Workouts: HKWorkoutBuilder
For recording active workout — only HKWorkoutBuilder, not old HKWorkout(activityType:start:end:). Builder allows adding samples realtime:
let config = HKWorkoutConfiguration()
config.activityType = .running
config.locationType = .outdoor
let builder = HKWorkoutBuilder(healthStore: healthStore, configuration: config, device: .local())
builder.beginCollection(withStart: Date()) { success, error in
// workout started
}
// every 5 seconds add heart rate
let heartRateSample = HKQuantitySample(
type: HKQuantityType(.heartRate),
quantity: HKQuantity(unit: .init(from: "count/min"), doubleValue: 142),
start: Date(), end: Date()
)
builder.add([heartRateSample]) { _, _ in }
// finish
builder.endCollection(withEnd: Date()) { _, _ in
builder.finishWorkout { workout, error in
// workout saved to HealthKit
}
}
Typical Errors
- HealthKit API call on MainActor without
async/await— UI blocks on slow queries to large datasets - Not checking
HKHealthStore.isHealthDataAvailable()— HealthKit unavailable on iPad without Apple Watch - Reading heart rate in units
count/mininstead ofHKUnit(from: "count/min")— result will be wrong
Timeframes
Basic integration reading steps, heart rate and workouts — 5–8 work days. With workout recording, background sync and permissions screen — 2–3 weeks.







