使用 HealthKit 讀取、寫入與查詢 Apple 健康資料。涵蓋 HKHealthStore 授權、樣本查詢、統計查詢、用於圖表的統計集合查詢、儲存 HKQuantitySample 資料、背景傳送、使用 HKWorkoutSession 與 HKLiveWorkoutBuilder 的體能訓練階段、HKUnit 以及 HKQuantityTypeIdentifier 值。適用於整合 Apple 健康、顯示健康指標、記錄體能訓練或啟用背景健康資料傳送。
HealthKit
從 Apple 健康資料庫讀取與寫入健康和體能資料。涵蓋授權、查詢、寫入樣本、背景傳送與體能訓練階段。目標為 Swift 6.3 / iOS 26+。
目錄
設定與可用性
專案設定
- 在 Xcode 中啟用 HealthKit 功能(會加入授權)
- 在 Info.plist 中加入
NSHealthShareUsageDescription(讀取)與NSHealthUpdateUsageDescription(寫入) - 若需背景傳送,啟用「Background Delivery」子功能
可用性檢查
在呼叫其他 HealthKit API 之前,務必檢查可用性。健康資料在 iOS、watchOS、visionOS、iPadOS 17+ 以及 Vision Pro 上執行的 iOS App 中可用。在 iPadOS 16 或更早版本上不可用,且可能受管理裝置政策限制。
import HealthKit
guard HKHealthStore.isHealthDataAvailable() else {
// 此裝置上健康資料不可用或受限制。
return
}
let healthStore = HKHealthStore()
建立單一 HKHealthStore 實例並在整個 App 中重複使用。它是執行緒安全的。如果 HealthKit 是選用的,請檢查 Xcode 產生的 UIRequiredDeviceCapabilities 中的 healthkit 項目,以免意外排除不支援的裝置。
授權
僅請求 App 真正需要的資料類型。App Review 會拒絕過度請求的 App。
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
)
}
檢查授權狀態
authorizationStatus(for:) 回報寫入/分享授權。HealthKit 不會透露讀取權限是否被授予或拒絕。如果使用者拒絕讀取權限,查詢只會傳回 App 成功儲存的樣本,這可能看起來像是空資料或部分資料。
let status = healthStore.authorizationStatus(
for: HKQuantityType(.stepCount)
)
switch status {
case .notDetermined:
// 尚未請求——可以安全呼叫 requestAuthorization
break
case .sharingAuthorized:
// 使用者授予寫入權限
break
case .sharingDenied:
// 使用者拒絕寫入權限(讀取拒絕與「無資料」無法區分)
break
@unknown default:
break
}
讀取資料:樣本查詢
使用 HKSampleQueryDescriptor(async/await)進行一次性讀取。偏好使用描述符而非舊的回呼式 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
}
// 從樣本中提取數值:
for sample in results {
let bpm = sample.quantity.doubleValue(
for: HKUnit.count().unitDivided(by: .minute())
)
print("\(bpm) bpm at \(sample.endDate)")
}
讀取資料:統計查詢
使用 HKStatisticsQueryDescriptor 進行聚合的單一數值統計(總和、平均值、最小值、最大值)。
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())
}
依資料類型的選項:
- 累積類型(步數、卡路里):
.cumulativeSum - 離散類型(心率、體重):
.discreteAverage、.discreteMin、.discreteMax
讀取資料:統計集合查詢
使用 HKStatisticsCollectionQueryDescriptor 取得按間隔分組的時間序列資料——非常適合圖表。
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
}
長時間執行的集合查詢
使用 results(for:)(複數)取得 AsyncSequence,當新資料到達時會發出更新:
let updateStream = query.results(for: healthStore)
Task {
for try await result in updateStream {
// result.statisticsCollection 包含更新後的資料
}
}
寫入資料
建立 HKQuantitySample 物件並儲存到資料庫。
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)
}
將 try await healthStore.save(sample) 回傳視為儲存成功的閘門;只有成功後才回報成功或推進 App 狀態。若失敗,顯示錯誤並修正已知的授權、類型、單位、持續時間或輸入問題,然後再建立另一個樣本。若需要持久性證據,在健康 App 中進行有限查詢或檢查可作為整合測試檢查,但並非每次儲存後的必要生產環境讀取。
您的 App 只能刪除自己建立的樣本。來自其他 App 或 Apple Watch 的樣本是唯讀的。
背景傳送
註冊背景更新,以便在新資料到達時啟動您的 App。需要背景傳送授權。
func enableStepCountBackgroundDelivery() async throws {
let stepType = HKQuantityType(.stepCount)
try await healthStore.enableBackgroundDelivery(
for: stepType,
frequency: .hourly
)
}
搭配 HKObserverQuery 處理通知。務必呼叫完成處理器:
let observerQuery = HKObserverQuery(
sampleType: HKQuantityType(.stepCount),
predicate: nil
) { query, completionHandler, error in
defer { completionHandler() } // 必須呼叫以表示完成
guard error == nil else { return }
// 取得新資料、更新 UI 等
}
healthStore.execute(observerQuery)
頻率: .immediate、.hourly、.daily、.weekly
在 App 啟動後立即設定觀察者查詢,然後對相同樣本類型呼叫一次 enableBackgroundDelivery。系統會持續保留註冊,在每個請求的頻率內最多喚醒 App 一次,並對某些類型(例如 iOS 上的每小時步數傳送)施加更嚴格的限制。背景傳送不支援模擬器;請在實體裝置上測試。
體能訓練階段
使用 HKWorkoutSession 和 HKLiveWorkoutBuilder 追蹤即時體能訓練。HKWorkoutSession 在 iOS/iPadOS 17+、visionOS 1+ 和 watchOS 2+ 上可用。HKLiveWorkoutBuilder 在 iOS/iPadOS 26+ 和 watchOS 5+ 上可用,因此若支援較舊的 iOS/iPadOS 版本,請對即時建構器程式碼進行閘門處理。
在 iPhone 和 iPad 上,即時心率收集需要配對的外部心率感測器。Apple Watch 階段可以收集高頻心率資料。對於鎖定的 iPhone 體能訓練,請在鎖定畫面上顯示健康指標之前,規劃系統的體能訓練資料存取流程。
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())
}
// 請求結束;從委派的 .stopped 轉換進行最終化。
session.stopActivity(with: Date())
不要在請求停止後立即呼叫 endCollection 和 finishWorkout。請等待階段委派的 .stopped 轉換,然後依序等待 builder.endCollection(at:) 和 builder.finishWorkout()。只有在兩個操作都回傳後,才將體能訓練標記為已儲存並清除階段狀態。處理每個拋出的錯誤,不要盲目重複結束程序。成功的 finishWorkout() 可能在裝置鎖定時回傳無體能訓練物件,因此單獨的 nil 結果並不代表失敗。
如需完整的體能訓練生命週期管理(包括暫停/恢復、委派處理和多裝置鏡像),請參閱 references/healthkit-patterns.md。
常見資料類型
HKQuantityTypeIdentifier
| 識別碼 | 類別 | 單位 |
|---|---|---|
.stepCount |
體能 | .count() |
.distanceWalkingRunning |
體能 | .meter() |
.activeEnergyBurned |
體能 | .kilocalorie() |
.basalEnergyBurned |
體能 | .kilocalorie() |
.heartRate |
生命徵象 | .count()/.minute() |
.restingHeartRate |
生命徵象 | .count()/.minute() |
.oxygenSaturation |
生命徵象 | .percent() |
.bodyMass |
身體 | .gramUnit(with: .kilo) |
.bodyMassIndex |
身體 | .count() |
.height |
身體 | .meter() |
.bodyFatPercentage |
身體 | .percent() |
.bloodGlucose |
檢驗 | .gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)) |
HKCategoryTypeIdentifier
常見類別類型:.sleepAnalysis、.mindfulSession、.appleStandHour
HKCharacteristicType
唯讀的使用者特徵包括 .dateOfBirth、.biologicalSex、.bloodType、.fitzpatrickSkinType、.wheelchairUse 和 .activityMoveMode。
HKUnit 參考
// 基本單位
HKUnit.count() // 步數、計數
HKUnit.meter() // 距離
HKUnit.mile() // 距離(英制)
HKUnit.kilocalorie() // 能量
HKUnit.joule(with: .kilo) // 能量(SI)
HKUnit.gramUnit(with: .kilo) // 質量(公斤)
HKUnit.pound() // 質量(英制)
HKUnit.percent() // 百分比
// 複合單位
HKUnit.count().unitDivided(by: .minute()) // 心率(bpm)
HKUnit.meter().unitDivided(by: .second()) // 速度(m/s)
// 前綴單位
HKUnit.gramUnit(with: .milli) // 毫克
HKUnit.literUnit(with: .deci) // 分升
常見錯誤
- 過度請求資料類型。 僅請求功能實際使用的讀取/寫入類型;廣泛的 HealthKit 權限表單是 App Review 的風險。
- 將讀取授權視為寫入授權。 您可以在儲存前檢查
.sharingAuthorized,但讀取拒絕受到隱私保護,看起來只會傳回 App 自己擁有的、空的或部分的結果。 - 跳過
isHealthDataAvailable()。 在 HealthKit 存取前檢查,並處理不可用或受限制的資料庫而不崩潰。 - 在新的非同步程式碼中使用回呼查詢。 對於一次性讀取和統計,偏好使用非同步描述符,並將大型查詢保留在主執行緒之外。
- 忘記觀察者完成處理器。 務必呼叫處理器;遺漏完成可能會延遲或停止未來的背景傳送。
- 假設
.immediate是立即的。 背景傳送受到系統限制,必須在實體裝置上測試。 - 對離散值使用累積統計。 將統計選項與資料類型匹配:步數/能量使用累積總和,心率、體重等類似樣本使用離散平均值/最小值/最大值。
審查清單
- [ ] 在任何 HealthKit 存取前檢查
HKHealthStore.isHealthDataAvailable() - [ ] 授權中僅請求必要的資料類型
- [ ] Info.plist 包含
NSHealthShareUsageDescription和/或NSHealthUpdateUsageDescription - [ ] 在 Xcode 專案中啟用 HealthKit 功能
- [ ] 儲存前檢查寫入授權;讀取拒絕視為部分或空的查詢結果
- [ ] 重複使用單一
HKHealthStore實例(不為每個查詢建立) - [ ] 使用非同步查詢描述符而非回呼式查詢
- [ ] 大型查詢不阻塞主執行緒
- [ ] 統計選項與資料類型匹配(累積 vs. 離散)
- [ ] 背景傳送搭配 App 啟動時的
HKObserverQuery設定,並呼叫completionHandler - [ ] 若使用
enableBackgroundDelivery,已啟用背景傳送授權 - [ ] 背景傳送已在實體裝置上測試,並考慮頻率限制
- [ ] 體能訓練停止等待委派的
.stopped轉換後才執行endCollection和finishWorkout;僅在成功最終化後清除狀態 - [ ] 處理體能訓練 API 可用性與即時心率感測器需求
- [ ] 刪除操作僅針對 App 先前儲存的物件




