
Healthkit Code Review
- 116 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
For integrating A developer tool for AI integration and automation
About
A developer tool for AI integration and automation. This is a developer tool for building and integrating AI-powered features.
- AI
- Developer tool
Healthkit Code Review by the numbers
- 116 all-time installs (skills.sh)
- Ranked #3,899 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill healthkit-code-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 116 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
What it does
For integrating A developer tool for AI integration and automation
Files
HealthKit Code Review
Quick Reference
| Issue Type | Reference |
|---|---|
| HKHealthStore, permissions, status checks, privacy | references/authorization.md |
| HKQuery types, predicates, anchored queries, statistics | references/queries.md |
| Background delivery, observer queries, completion handlers | references/background.md |
| HKQuantityType, HKCategoryType, workouts, units | references/data-types.md |
Review Checklist
- [ ]
HKHealthStore.isHealthDataAvailable()called before any HealthKit operations - [ ] Authorization requested only for needed data types (minimal permissions)
- [ ]
requestAuthorizationcompletion handler not misinterpreted as permission granted - [ ] No attempt to determine read permission status (privacy by design)
- [ ] Query results dispatched to main thread for UI updates
- [ ]
HKObjectQueryNoLimitused only with bounded predicates - [ ]
HKStatisticsQueryused for aggregations instead of manual summing - [ ] Observer query
completionHandler()always called (usedefer) - [ ] Background delivery registered in
application(_:didFinishLaunchingWithOptions:) - [ ] Background delivery entitlement added (iOS 15+)
- [ ] Correct units used for quantity types (e.g.,
count/minfor heart rate) - [ ] Long-running queries stored as properties and stopped in
deinit
When to Load References
- Reviewing authorization/permissions flow -> authorization.md
- Reviewing HKSampleQuery, HKAnchoredObjectQuery, or predicates -> queries.md
- Reviewing HKObserverQuery or
enableBackgroundDelivery-> background.md - Reviewing HKQuantityType, HKCategoryType, or HKWorkout -> data-types.md
Review gates
Run in order. Do not state a finding in a later step until the pass condition for the current step is satisfied (each pass condition is answerable from the codebase under review).
1. Scope — Pass: Name the file path(s) and types/symbols using HealthKit, HKHealthStore, or HK* APIs (or state clearly that the diff touches none). 2. Availability and store — Pass: Cite the call site of isHealthDataAvailable() before HealthKit use, or document why omission is acceptable for the scoped code; cite where HKHealthStore is created or injected. 3. Authorization semantics — Pass: For each requestAuthorization / getRequestStatusForAuthorization, cite handler branches per references/authorization.md (e.g. success does not prove read access); do not infer read permission from authorizationStatus alone. 4. Queries and limits — Pass: For each query, cite predicate + limit (HKObjectQueryNoLimit only with a bounded predicate); for totals/aggregates, cite HKStatisticsQuery / collection vs manual summing per references/queries.md. 5. Observers and background — Pass: If HKObserverQuery or enableBackgroundDelivery appears, cite where the observer is started/stopped and where background delivery is registered; cite entitlements/Info.plist or flag missing config per references/background.md. If absent, Pass: one line “no observer/background in scope.” 6. Threading and lifecycle — Pass: Cite main-queue (or documented pattern) for UI updates from query callbacks; cite retention/stop()/deinit for long-running queries per checklist above.
Review Questions
1. Is isHealthDataAvailable() checked before creating HKHealthStore? 2. Does the code gracefully handle denied permissions (empty results)? 3. Are observer query completion handlers called in all code paths? 4. Is work in background handlers minimal (~15 second limit)? 5. Are HKQueryAnchors persisted per sample type (not shared)?
HealthKit Authorization
Authorization Model
HealthKit uses per-data-type, separate read/write authorization. Users grant access individually for each health data type.
Required Setup
1. Entitlement: com.apple.developer.healthkit capability 2. Info.plist Keys:
NSHealthShareUsageDescription- Why app needs to read health dataNSHealthUpdateUsageDescription- Why app needs to write health data
HKAuthorizationStatus (Write Only)
| Status | Meaning |
|---|---|
.notDetermined | User hasn't been asked yet |
.sharingAuthorized | User granted write access |
.sharingDenied | User explicitly denied write access |
Critical: Read permission status is intentionally hidden for privacy.
Key Methods
| Method | Purpose |
|---|---|
isHealthDataAvailable() | Check device support (not on iPad pre-iPadOS 17) |
authorizationStatus(for:) | Check write permission only |
getRequestStatusForAuthorization(toShare:read:) | Check if auth sheet would appear |
requestAuthorization(toShare:read:) | Request permissions from user |
Critical Anti-Patterns
1. Misinterpreting requestAuthorization Success
// BAD: success does NOT mean permission granted
healthStore.requestAuthorization(toShare: types, read: types) { success, error in
if success {
self.startRecordingWorkout() // May fail if permission denied!
}
}
// GOOD: success means dialog flow completed
healthStore.requestAuthorization(toShare: types, read: types) { success, error in
if success {
// Check specific type for write operations
if self.healthStore.authorizationStatus(for: workoutType) == .sharingAuthorized {
self.startRecordingWorkout()
}
}
}2. Checking Read Permission Status
// BAD: Cannot determine read permission status
let status = healthStore.authorizationStatus(for: heartRateType)
if status == .sharingAuthorized { // This only checks WRITE status!
displayHeartRateData()
}
// GOOD: Just attempt to fetch - empty results if denied
func fetchHeartRateData() async {
let results = try? await healthStore.execute(query)
// Handle empty results gracefully
}3. Not Checking Device Availability
// BAD: HealthKit not available on iPad (pre-iPadOS 17)
func requestHealthKitAccess() {
healthStore.requestAuthorization(toShare: types, read: types) { _, _ in }
}
// GOOD: Always check availability first
func requestHealthKitAccess() {
guard HKHealthStore.isHealthDataAvailable() else {
showHealthKitNotAvailableMessage()
return
}
healthStore.requestAuthorization(toShare: types, read: types) { _, _ in }
}4. Widgets Requesting Authorization
// BAD: Widgets cannot present authorization UI
struct HealthWidget: Widget {
func getTimeline(...) {
healthStore.requestAuthorization(...) // Will silently fail
}
}
// GOOD: Use getRequestStatusForAuthorization in widgets
struct HealthWidget: Widget {
func getTimeline(...) {
let status = try? await healthStore.statusForAuthorizationRequest(toShare: [], read: types)
if status == .shouldRequest {
showOpenAppPrompt()
} else {
displayHealthData() // May be empty if denied
}
}
}5. Accessing Data Before Authorization Completes
// BAD: Race condition (HKError code 5)
func initialize() {
healthStore.requestAuthorization(toShare: types, read: types) { _, _ in }
fetchHealthData() // Called before authorization completes!
}
// GOOD: Wait for authorization
func initialize() async {
do {
try await healthStore.requestAuthorization(toShare: types, read: types)
await fetchHealthData()
} catch {
handleAuthorizationError(error)
}
}HKError Codes
| Code | Constant | Meaning |
|---|---|---|
| - | .errorAuthorizationDenied | User denied permission |
| - | .errorAuthorizationNotDetermined | App hasn't requested yet |
| 4 | - | Missing HealthKit entitlement |
| 5 | - | Transaction failed (often premature data access) |
Review Questions
1. Is isHealthDataAvailable() called before any HealthKit operations? 2. Is the requestAuthorization completion handler correctly interpreted? 3. Is there any attempt to determine read permission status? (anti-pattern) 4. Are all Info.plist usage description keys present with meaningful text? 5. Do widgets avoid calling requestAuthorization? 6. Is authorization status re-checked before write operations (not cached)?
HealthKit Background Delivery
Overview
Background delivery allows apps to receive HealthKit updates without user launching the app. Requires:
1. `enableBackgroundDelivery(for:frequency:)` - Register for notifications 2. `HKObserverQuery` - Long-running query that monitors changes
Required Configuration
Entitlements (iOS 15+/Xcode 13+)
<key>com.apple.developer.healthkit</key>
<true/>
<key>com.apple.developer.healthkit.background-delivery</key>
<true/>Capabilities
- HealthKit > Background Delivery
- Background Modes > Background Processing
Update Frequencies
| Frequency | Behavior | Reality |
|---|---|---|
.immediate | Wake on every change | Some types enforce hourly max (stepCount) |
.hourly | At most once per hour | iOS may defer based on battery/CPU |
.daily | At most once per day | Advisory, not guaranteed |
.weekly | At most once per week | Advisory, not guaranteed |
iOS has full discretion to defer based on CPU, battery, connectivity, Low Power Mode.
Background Execution Constraints
- Time limit: ~15 seconds for simple queries
- 3 strikes rule: After 3 failed completions, delivery stops
- watchOS budget: 4 updates/hour (shared with WKApplicationRefreshBackgroundTask)
Required Setup Pattern
// AppDelegate.swift - MUST be in didFinishLaunchingWithOptions
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [...]) -> Bool {
setupHealthKitBackgroundDelivery()
return true
}
func setupHealthKitBackgroundDelivery() {
let stepType = HKQuantityType(.stepCount)
// 1. Create observer query
let query = HKObserverQuery(sampleType: stepType, predicate: nil) {
query, completionHandler, error in
defer { completionHandler() } // MUST always call
guard error == nil else { return }
self.fetchNewData() // Keep minimal - 15 sec limit
}
// 2. Execute query
healthStore.execute(query)
// 3. Enable background delivery
healthStore.enableBackgroundDelivery(for: stepType, frequency: .immediate) { _, _ in }
}Critical Anti-Patterns
1. Not Calling completionHandler
// BAD: completionHandler not called on error path
let query = HKObserverQuery(...) { query, completionHandler, error in
if let error = error {
print("Error: \(error)")
return // completionHandler not called!
}
self.fetchData()
completionHandler()
}
// GOOD: Use defer to ensure it's always called
let query = HKObserverQuery(...) { query, completionHandler, error in
defer { completionHandler() } // Always called
guard error == nil else {
print("Error: \(error!)")
return
}
self.fetchData()
}2. Not Re-registering on App Launch
// BAD: Registering only once
class HealthManager {
private var hasRegistered = false
func setupBackgroundDelivery() {
guard !hasRegistered else { return } // Won't work after app update!
hasRegistered = true
// ...
}
}
// GOOD: Always register in didFinishLaunchingWithOptions
// Registration must happen every app launch
func application(_ app: UIApplication, didFinishLaunchingWithOptions: [...]) -> Bool {
healthManager.setupBackgroundDelivery() // Called every launch
return true
}3. Local Variable Query Gets Deallocated
// BAD: Query goes out of scope
func setupObserver() {
let query = HKObserverQuery(...)
healthStore.execute(query)
} // query deallocated!
// GOOD: Store as property
class HealthManager {
private var observerQueries: [HKObserverQuery] = []
func setupObserver() {
let query = HKObserverQuery(...)
observerQueries.append(query) // Keep reference
healthStore.execute(query)
}
}4. Assuming Callback Means New Data
// BAD: Callback fires even without data changes
let query = HKObserverQuery(...) { query, completionHandler, error in
defer { completionHandler() }
self.processNewData() // May process nothing - called on foreground too
}
// GOOD: Use HKAnchoredObjectQuery to get actual changes
func fetchChanges(completion: @escaping () -> Void) {
let query = HKAnchoredObjectQuery(type: type, anchor: savedAnchor, ...) {
query, samples, deleted, newAnchor, error in
defer { completion() }
guard let samples = samples, let newAnchor = newAnchor else { return }
self.anchor = newAnchor // Persist
// Only process actual new samples
self.process(samples: samples, deleted: deleted ?? [])
}
healthStore.execute(query)
}5. Long-Running Work in Background Handler
// BAD: May exceed 15-second time limit
let query = HKObserverQuery(...) { query, completionHandler, error in
self.downloadFromServer()
self.processAllHistoricalData()
self.syncToCloudKit()
completionHandler()
}
// GOOD: Minimal work, schedule full sync for later
let query = HKObserverQuery(...) { query, completionHandler, error in
defer { completionHandler() } // Complete quickly
// Just fetch latest
self.fetchLatestSample { sample in
self.pendingSamples.append(sample)
// Schedule full sync when app is active
DispatchQueue.main.async {
if UIApplication.shared.applicationState == .active {
self.performFullSync()
}
}
}
}6. Missing Required Entitlement
<!-- BAD: Missing background delivery entitlement -->
<key>com.apple.developer.healthkit</key>
<true/>
<!-- GOOD: All required entitlements -->
<key>com.apple.developer.healthkit</key>
<true/>
<key>com.apple.developer.healthkit.background-delivery</key>
<true/>Review Questions
1. Is enableBackgroundDelivery called in application(_:didFinishLaunchingWithOptions:)? 2. Are all required entitlements configured (especially iOS 15+)? 3. Are observer queries stored as instance properties? 4. Is completionHandler() called in ALL code paths? 5. Is work in the observer callback minimal (~15 seconds)? 6. Does code handle that callbacks may fire without actual data changes? 7. Are anchors persisted to track processed data? 8. Is the update frequency appropriate for the data type?
HealthKit Data Types
Data Type Hierarchy
| Type | Class | Purpose |
|---|---|---|
| Quantity | HKQuantityType / HKQuantitySample | Measurable numeric values with units |
| Category | HKCategoryType / HKCategorySample | Enumerated categorical values |
| Correlation | HKCorrelationType / HKCorrelation | Groups of related samples |
| Characteristic | HKCharacteristicType | Static, immutable user data |
| Workout | HKWorkout / HKWorkoutBuilder | Exercise sessions |
HKQuantityType - Measurable Data
Common quantity types and their units:
| Type | Unit |
|---|---|
.stepCount | .count() |
.heartRate | HKUnit(from: "count/min") |
.distanceWalkingRunning | .meter() |
.activeEnergyBurned | .kilocalorie() |
.bloodPressureSystolic | .millimeterOfMercury() |
.oxygenSaturation | .percent() |
Creating HKQuantitySample
// 1. Get quantity type
guard let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else { return }
// 2. Create quantity with unit
let quantity = HKQuantity(unit: .count(), doubleValue: 10000)
// 3. Create sample with metadata
let metadata: [String: Any] = [
HKMetadataKeyTimeZone: TimeZone.current.identifier,
HKMetadataKeyWasUserEntered: false
]
let sample = HKQuantitySample(type: stepType, quantity: quantity,
start: startDate, end: endDate, metadata: metadata)
// 4. Save
healthStore.save(sample) { success, error in }HKCategoryType - Enumerated Data
Sleep Analysis (iOS 16+)
let sleepType = HKCategoryType(.sleepAnalysis)
// Create both in-bed and asleep samples
let inBedSample = HKCategorySample(
type: sleepType,
value: HKCategoryValueSleepAnalysis.inBed.rawValue,
start: bedTime, end: wakeTime
)
let asleepSample = HKCategorySample(
type: sleepType,
value: HKCategoryValueSleepAnalysis.asleepCore.rawValue, // iOS 16+
start: fallAsleepTime, end: actualWakeTime
)
healthStore.save([inBedSample, asleepSample]) { _, _ in }HKCorrelation - Blood Pressure
Blood pressure requires a correlation with both systolic and diastolic:
// Create systolic sample
let systolicType = HKQuantityType(.bloodPressureSystolic)
let systolicSample = HKQuantitySample(
type: systolicType,
quantity: HKQuantity(unit: .millimeterOfMercury(), doubleValue: 120),
start: date, end: date
)
// Create diastolic sample
let diastolicType = HKQuantityType(.bloodPressureDiastolic)
let diastolicSample = HKQuantitySample(
type: diastolicType,
quantity: HKQuantity(unit: .millimeterOfMercury(), doubleValue: 80),
start: date, end: date
)
// Create correlation
let bpType = HKCorrelationType(.bloodPressure)
let bpCorrelation = HKCorrelation(
type: bpType, start: date, end: date,
objects: [systolicSample, diastolicSample]
)Important: Request permissions on systolic/diastolic types, NOT the correlation type.
HKCharacteristicType - Static Data
Read-only characteristics set by user in Health app:
do {
let biologicalSex = try healthStore.biologicalSex().biologicalSex
switch biologicalSex {
case .female, .male, .other: // handle
case .notSet: // user hasn't set
@unknown default: break
}
} catch {
// Permission denied or not set
}HKWorkoutBuilder Pattern
let config = HKWorkoutConfiguration()
config.activityType = .running
config.locationType = .outdoor
let builder = HKWorkoutBuilder(healthStore: healthStore, configuration: config, device: .local())
try await builder.beginCollection(at: startDate)
// Add samples during workout
try await builder.addSamples([heartRateSample, distanceSample])
// Finish
try await builder.endCollection(at: endDate)
let workout = try await builder.finishWorkout()Critical Anti-Patterns
1. Wrong Units for Quantity Types
// BAD: Incompatible units
let heartRate = HKQuantity(unit: .count(), doubleValue: 72) // Wrong!
let distance = HKQuantity(unit: .kilocalorie(), doubleValue: 5000) // Wrong!
// GOOD: Correct compatible units
let heartRate = HKQuantity(unit: HKUnit(from: "count/min"), doubleValue: 72)
let distance = HKQuantity(unit: .meter(), doubleValue: 5000)2. Force Unwrapping Quantity Types
// BAD: Can crash
let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
// GOOD: Safe unwrapping
guard let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
print("Step count type unavailable")
return
}3. Requesting Correlation Type Permission
// BAD: Cannot request permission for correlation types
let bpType = HKCorrelationType(.bloodPressure)
let typesToRead: Set<HKObjectType> = [bpType] // Will fail!
// GOOD: Request underlying quantity types
let systolicType = HKQuantityType(.bloodPressureSystolic)
let diastolicType = HKQuantityType(.bloodPressureDiastolic)
let typesToRead: Set<HKObjectType> = [systolicType, diastolicType]4. Not Including Time Zone Metadata
// BAD: No time zone
let sample = HKQuantitySample(type: type, quantity: quantity, start: start, end: end)
// GOOD: Include time zone
let metadata: [String: Any] = [HKMetadataKeyTimeZone: TimeZone.current.identifier]
let sample = HKQuantitySample(type: type, quantity: quantity,
start: start, end: end, metadata: metadata)5. Reading Characteristics Without Error Handling
// BAD: Force try
let sex = try! healthStore.biologicalSex().biologicalSex
// GOOD: Handle errors
do {
let sex = try healthStore.biologicalSex().biologicalSex
// Check for .notSet
} catch {
// Handle permission denied or not set
}6. Only Creating InBed Sleep Sample
// BAD: Missing actual sleep sample
let inBedSample = HKCategorySample(type: sleepType,
value: HKCategoryValueSleepAnalysis.inBed.rawValue, ...)
// No asleep sample!
// GOOD: Create both
let inBedSample = HKCategorySample(type: sleepType,
value: HKCategoryValueSleepAnalysis.inBed.rawValue,
start: bedTime, end: wakeTime)
let asleepSample = HKCategorySample(type: sleepType,
value: HKCategoryValueSleepAnalysis.asleepCore.rawValue,
start: sleepTime, end: wakeTime)
healthStore.save([inBedSample, asleepSample]) { _, _ in }Unit Conversion
// HealthKit handles conversion automatically
let distanceInMeters = sample.quantity.doubleValue(for: .meter())
let distanceInMiles = sample.quantity.doubleValue(for: .mile())
// User preferred units
healthStore.preferredUnits(for: [stepType]) { units, error in
let preferredUnit = units[stepType]
}Review Questions
1. Is isHealthDataAvailable() checked before creating HKHealthStore? 2. Are quantity types used with compatible units? 3. Are correlation types (blood pressure) created with all required sub-samples? 4. Are permissions requested on underlying types, not correlation types? 5. Are characteristics read with proper error handling for .notSet? 6. Is metadata included (time zone, sync identifier)? 7. Are sleep samples created correctly (both inBed and asleep)? 8. Is HKWorkoutBuilder used instead of deprecated HKWorkout init?
HealthKit Queries
Query Types Overview
| Query Type | Use Case | Long-Running | Returns Deletions |
|---|---|---|---|
HKSampleQuery | One-time snapshot | No | No |
HKAnchoredObjectQuery | Incremental sync, change tracking | Yes (with updateHandler) | Yes |
HKStatisticsQuery | Single aggregation (sum, avg, min, max) | No | N/A |
HKStatisticsCollectionQuery | Time-series aggregations | Yes | N/A |
HKActivitySummaryQuery | Activity rings data | Yes (optional) | No |
HKSampleQuery
Basic one-time fetch with sorting:
let query = HKSampleQuery(
sampleType: sampleType,
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)]
) { (query, samples, error) in
DispatchQueue.main.async {
// Handle results - update UI
}
}
healthStore.execute(query)HKAnchoredObjectQuery
For incremental sync with deletion tracking:
let query = HKAnchoredObjectQuery(
type: sampleType,
predicate: predicate,
anchor: savedAnchor, // nil for first fetch, persisted for subsequent
limit: HKObjectQueryNoLimit
) { (query, samples, deletedObjects, newAnchor, error) in
// Process samples AND deletedObjects
self.saveAnchor(newAnchor) // Persist for next query
}
// Optional: continuous monitoring
query.updateHandler = { (query, samples, deleted, newAnchor, error) in }
healthStore.execute(query)Important: HKQueryAnchor cannot be reused across different sample types.
HKStatisticsQuery
For aggregations - use correct options per data type:
| Data Type | Valid Options |
|---|---|
| Cumulative (steps, distance) | .cumulativeSum |
| Discrete (weight, heart rate) | .discreteAverage, .discreteMin, .discreteMax |
let query = HKStatisticsQuery(
quantityType: stepCountType,
quantitySamplePredicate: predicate,
options: .cumulativeSum // Use .discreteAverage for body mass
) { (query, statistics, error) in
let sum = statistics?.sumQuantity()?.doubleValue(for: .count())
}Predicate Building
let predicate = HKQuery.predicateForSamples(
withStart: startDate,
end: endDate,
options: [.strictStartDate, .strictEndDate]
)
// .strictStartDate: Sample start >= startDate
// .strictEndDate: Sample end <= endDate
// [] (empty): Sample overlaps with rangeCritical Anti-Patterns
1. HKObjectQueryNoLimit Without Predicates
// BAD: May fetch millions of samples - memory exhaustion
let query = HKSampleQuery(
sampleType: stepCountType,
predicate: nil,
limit: HKObjectQueryNoLimit,
sortDescriptors: nil
) { ... }
// GOOD: Always bound with predicates
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate)
let query = HKSampleQuery(
sampleType: stepCountType,
predicate: predicate,
limit: 1000, // Safety net
sortDescriptors: [...]
) { ... }2. Manual Summing Instead of Statistics Query
// BAD: Inefficient for cumulative data
let query = HKSampleQuery(...) { query, samples, error in
var total = 0.0
for sample in samples as? [HKQuantitySample] ?? [] {
total += sample.quantity.doubleValue(for: .count())
}
}
// GOOD: Use HKStatisticsQuery
let query = HKStatisticsQuery(
quantityType: stepCountType,
quantitySamplePredicate: predicate,
options: .cumulativeSum
) { query, statistics, error in
let total = statistics?.sumQuantity()?.doubleValue(for: .count())
}3. Not Handling Deleted Samples
// BAD: Ignoring deletions in anchored query
let query = HKAnchoredObjectQuery(...) { query, samples, deletedObjects, newAnchor, error in
for sample in samples ?? [] {
self.syncToServer(sample)
}
// Missing deletion handling!
}
// GOOD: Process both additions and deletions
let query = HKAnchoredObjectQuery(...) { query, samples, deletedObjects, newAnchor, error in
for sample in samples ?? [] {
self.syncToServer(sample)
}
for deleted in deletedObjects ?? [] {
self.deleteFromServer(deleted.uuid) // Critical for data integrity
}
self.saveAnchor(newAnchor)
}4. Reusing Anchors Across Sample Types
// BAD: Single anchor for all types
var anchor: HKQueryAnchor?
func syncWorkouts() { HKAnchoredObjectQuery(type: workoutType, anchor: anchor, ...) }
func syncSteps() { HKAnchoredObjectQuery(type: stepType, anchor: anchor, ...) } // Wrong!
// GOOD: Separate anchor per type
var anchors: [String: HKQueryAnchor] = [:]
func sync(type: HKSampleType) {
let query = HKAnchoredObjectQuery(type: type, anchor: anchors[type.identifier], ...) {
query, samples, deleted, newAnchor, error in
self.anchors[type.identifier] = newAnchor
}
}5. UI Updates on Background Thread
// BAD: Query handler runs on background thread
let query = HKSampleQuery(...) { query, samples, error in
self.tableView.reloadData() // Crash or undefined behavior
}
// GOOD: Dispatch to main thread
let query = HKSampleQuery(...) { query, samples, error in
DispatchQueue.main.async {
self.tableView.reloadData()
}
}6. Not Stopping Long-Running Queries
// BAD: Query never stopped - memory leak
class HealthVC: UIViewController {
var query: HKObserverQuery?
override func viewDidLoad() {
query = HKObserverQuery(...)
healthStore.execute(query!)
} // Query continues forever
}
// GOOD: Stop in deinit
class HealthVC: UIViewController {
var query: HKObserverQuery?
override func viewDidLoad() {
query = HKObserverQuery(...)
healthStore.execute(query!)
}
deinit {
if let query = query { healthStore.stop(query) }
}
}Review Questions
1. Is the correct query type used for the use case? 2. Are date predicates used to bound query results? 3. Are UI updates dispatched to the main thread? 4. Is HKObjectQueryNoLimit used with appropriate predicates? 5. Are deleted objects handled in anchored queries? 6. Are anchors persisted separately per sample type? 7. Are long-running queries stopped when no longer needed? 8. Are correct statistics options used for the data type?