【Swift/watchOS/iOS】HealthKitで睡眠分析(SleepAnalysis)を行う方法!
この記事からわかること
- Swift/watchOS/iOSでHealthKitフレームワークの使い方
- 睡眠分析(SleepAnalysis)データの取得
- HKCategoryValueSleepAnalysisクラスとは?
index
[open]
\ アプリをリリースしました /
環境
- Xcode:26.0.1
- iOS:26
- Swift:6
- watchOS:26.2
- macOS:Tahoe 26.0.1
HealthKitとは?
「HealthKit」とはiOS・watchOSにおいて健康データやフィットネスデータを管理、表示、共有機能など提供するフレームワークです。HealthKitフレームワークを使用することでデフォルトで入っている「ヘルスケア」や「フィットネス」アプリのデータを参照できるようになります。開発したアプリからデータを参照するためにはユーザーの明示的な許可が必要になります。
- 健康データ:身長や体重、心拍、睡眠などに関するデータ
- フィットネスデータ:歩数、距離、消費カロリー、ワークアウト(※)などに関するデータ
※ ワークアウトとはウォーキングやランニングなど特定の運動のパフォーマンスデータを追跡するための活動セッションのことです。Apple Watchには「ワークアウト」アプリがあり特定の運動を計測することが可能です。
睡眠分析データ
HealthKitの中には睡眠に関するデータも存在します。SleepAnalysisとして定義されており、iOS16以降から睡眠状態も以下のように段階で詳細に取得できるようになりました。Swiftとしては列挙型HKCategoryValueSleepAnalysisとして定義されています。
- inBed(0):ベッドの中
- asleepUnspecified(1):睡眠中(不明)
- awake(2):起床
- asleepCore(3):コア睡眠(浅いノンレム睡眠)
- asleepDeep(4):深い睡眠
- asleepREM(5):レム睡眠
・レム睡眠・・・「脳は活動(夢を見る・記憶整理)、体は休息(筋肉弛緩)」の状態
・コア睡眠・・・「脳も体も浅く休んでいる」状態
またwatchOS11から昼寝も検知できるようになったそうです。(参考記事)
アプリから見たときの睡眠分析データ
睡眠分析データの取得は日付単位で睡眠状態を取得することが可能になっています。睡眠分析データはHKCategorySample型としてデータが取得することができ、valueからHKCategoryValueSleepAnalysisの値(睡眠状態)やcategoryTypeでHKCategoryTypeIdentifierSleepAnalyticsかどうかを識別することができるようになっています。
またデータは観測可能になっています。デフォルトではフォアグラウンド時のみしかデータの更新通知(新規取得等)を検知できませんが、バックグラウンドでも更新通知を受信するようにアプリを設定することもできます。しかし端末自体がロックされている時はアプリ自体が起動しないのでリアルタイムでの検知はできず、アプリのロックが解除されたタイミングでデータが反映されるようになります。なので起床したタイミングに即座に何かしらアクションを起こすといったことはできないようです。
参考記事:進化したHealthKitを使って睡眠分析アプリを作ってみる
睡眠分析データを取得する処理の実装方法
HealthKitを使用した基本的な実装方法は以下の記事を参考にしてください。この記事では基本的なところは割愛して睡眠分析データを取得するところにフォーカスを当ててまとめていきます。
睡眠分析データの取得許可をユーザーから得るためにHKCategoryTypeIdentifier.sleepAnalysisを追加します。
/// 読み取り許可申請項目
public let readAllTypes: Set = [
// 睡眠分析
HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier.sleepAnalysis)!
]
あとはrequestAuthorizationで許可申請を実行します。
try await healthStore.requestAuthorization(toShare: writeAllTypes, read: readAllTypes)
実際に睡眠分析データを取得してみます。HKSampleQueryを使用してHKObjectType.categoryType(forIdentifier: .sleepAnalysis)を指定してサンプルデータを取得します。睡眠分析データはHKCategorySample型になります。実際の睡眠ステータスはvalueプロパティにHKCategoryValueSleepAnalysisとして格納されています。
/// 睡眠分析データを取得する
private func fetchSleepAnalysis() {
// 睡眠分析
guard let sampleType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else { return }
let query = HKSampleQuery(
sampleType: sampleType,
// 昨日の睡眠分析データを対象とする
predicate: createYesterdayPredicate(),
limit: HKObjectQueryNoLimit,
sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: true)]
) { [weak self] query, results, error in
guard let self else { return }
guard error == nil else { self.addLog("error"); return }
// HKCategorySample型かチェック
guard let results = results as? [HKCategorySample] else { self.addLog("error"); return }
self.addLog("睡眠分析データ取得完了")
let reversedResults = results.reversed()
// 取得した睡眠分析データの中身を表示する
reversedResults.forEach { s in
self.addLog("睡眠;\(s.categoryType)")
let start = jstFormatter.string(from: s.startDate)
let end = jstFormatter.string(from: s.endDate)
self.addLog("睡眠:\(s.value) \(start)〜\(end)")
}
}
healthStore.execute(query)
}
睡眠分析データの変更を観測する
睡眠分析データの変更は観測できるようになっています。観測するためにはHKObserverQueryを使用します。変化が起きたタイミングでHKObserverQueryのHKObserverQueryCompletionHandlerが実行されるのでその中で必要な処理を記述します。
/// 睡眠分析データを観測する
func observeSleepAnalysis() {
// 睡眠分析
guard let sampleType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else { return }
let observerQuery = HKObserverQuery(
sampleType: sampleType,
predicate: nil
) { [weak self] _, completionHandler, error in
guard let self else { return }
guard error == nil else { self.addLog("error"); return }
self.addLog("睡眠データ追加された")
// 睡眠データが変更されたときに実行したい処理
self.fetchDiff(completion: completionHandler)
}
healthStore.execute(observerQuery)
}
またデフォルトでは変更の検知はフォアグラウンド時にのみになっています。バックグラウンドでも取得できるように胃したい場合は「Signing & Capabilities」に追加した「HealthKit」の「HealthKit Background Delivery」にチェックを入れておく必要があります。チェックを入れた状態でenableBackgroundDeliveryを実行することでバックグラウンドでの取得を有効化することができます。
// バックグランドでのヘルスケアデータの更新検知を有効にする
healthStore.enableBackgroundDelivery(for: sampleType, frequency: .immediate) { [weak self] success, error in
guard let self else { return }
if let error = error {
self.addLog("\(error)")
}
self.addLog("バックグラウンド検知有効:\(success)")
}
睡眠の変化を検知した際に変化したデータを取得する方法がいまいちよくわかりませんでした。1つ目の方法として変更があったときに直近1時間のデータを再度取得する場合は以下のように実装できました。
/// 追加された睡眠分析データの取得
private func fetchDiff(completion: @escaping () -> Void) {
// 睡眠分析
guard let sampleType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else { return }
let query = HKSampleQuery(
sampleType: sampleType,
predicate: createDiffPredicate(),
limit: HKObjectQueryNoLimit,
sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: true)]
) { [weak self] query, results, error in
guard let self else { completion(); return }
guard error == nil else {
completion()
return
}
let samples = (results as? [HKCategorySample]) ?? []
let reversedResults = samples.reversed()
reversedResults.forEach { s in
let start = jstFormatter.string(from: s.startDate)
let end = jstFormatter.string(from: s.endDate)
self.addLog("睡眠:\(s.value) \(start)〜\(end)")
if self.isSleeping(s) {
// 睡眠中に何らかのアクション等
}
}
completion()
}
healthStore.execute(query)
}
private func isSleeping(_ sample: HKCategorySample) -> Bool {
switch sample.value {
case HKCategoryValueSleepAnalysis.asleepUnspecified.rawValue,
HKCategoryValueSleepAnalysis.asleepCore.rawValue,
HKCategoryValueSleepAnalysis.asleepDeep.rawValue,
HKCategoryValueSleepAnalysis.asleepREM.rawValue:
return true
default:
return false
}
}
もう1つの方法としてHKAnchoredObjectQueryを使用した方法も試してみました。ただこれはすでに取得したデータは再度取得しないようにするために使用できるAPIのようでアンカーを指標として、取得するデータを識別しているみたいです。データの取得は基本的に古いものから取得するようになっているみたいなので、最新のデータだけを取得のようにうまく実装することができませんでした。
/// 追加された睡眠分析データの取得
private func fetchSleepDiff(completion: @escaping () -> Void) {
// 睡眠分析
guard let sampleType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else { return }
// 初回は最古のデータを10件のみ取得しアンカーを進めておく
let query = HKAnchoredObjectQuery(
type: sampleType,
predicate: nil,
anchor: sleepAnchor,
limit: sleepAnchor == nil ? 10 : HKObjectQueryNoLimit
) { [weak self] _, addedSamples, _, newAnchor, error in
guard let self else { completion(); return }
// 初回(アンカーがnil)なら処理を終了する
if self.sleepAnchor == nil {
self.sleepAnchor = newAnchor
self.addLog("初回取得のため終了")
completion()
return
}
self.sleepAnchor = newAnchor
self.addLog("\(String(describing: sleepAnchor?.description))")
let samples = (addedSamples as? [HKCategorySample]) ?? []
for s in samples {
let start = jstFormatter.string(from: s.startDate)
let end = jstFormatter.string(from: s.endDate)
self.addLog("睡眠:\(s.value) \(start)〜\(end)")
}
completion()
}
healthStore.execute(query)
}
全体のサンプルコードは以下にまとめてあります。
おすすめ記事:GitHub iOS-HealthKitTest
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。





