【Swift/watchOS/iOS】HealthKitで睡眠分析(SleepAnalysis)を行う方法!

【Swift/watchOS/iOS】HealthKitで睡眠分析(SleepAnalysis)を行う方法!

この記事からわかること

  • Swift/watchOS/iOSHealthKitフレームワーク使い方
  • 睡眠分析(SleepAnalysis)データ取得
  • HKCategoryValueSleepAnalysisクラスとは?

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

HealthKitとは?

HealthKit」とはiOS・watchOSにおいて健康データやフィットネスデータを管理、表示、共有機能など提供するフレームワークです。HealthKitフレームワークを使用することでデフォルトで入っている「ヘルスケア」や「フィットネス」アプリのデータを参照できるようになります。開発したアプリからデータを参照するためにはユーザーの明示的な許可が必要になります。

※ ワークアウトとはウォーキングやランニングなど特定の運動のパフォーマンスデータを追跡するための活動セッションのことです。Apple Watchには「ワークアウト」アプリがあり特定の運動を計測することが可能です。

【Swift】HealthKitの導入方法と使い方!

引用 公式リファレンス:HealthKit

睡眠分析データ

HealthKitの中には睡眠に関するデータも存在します。SleepAnalysisとして定義されており、iOS16以降から睡眠状態も以下のように段階で詳細に取得できるようになりました。Swiftとしては列挙型HKCategoryValueSleepAnalysisとして定義されています。

レム睡眠・・・「脳は活動(夢を見る・記憶整理)、体は休息(筋肉弛緩)」の状態
コア睡眠・・・「脳も体も浅く休んでいる」状態

またwatchOS11から昼寝も検知できるようになったそうです。(参考記事)

アプリから見たときの睡眠分析データ

睡眠分析データの取得は日付単位で睡眠状態を取得することが可能になっています。睡眠分析データはHKCategorySample型としてデータが取得することができ、valueからHKCategoryValueSleepAnalysisの値(睡眠状態)やcategoryTypeHKCategoryTypeIdentifierSleepAnalyticsかどうかを識別することができるようになっています。

またデータは観測可能になっています。デフォルトではフォアグラウンド時のみしかデータの更新通知(新規取得等)を検知できませんが、バックグラウンドでも更新通知を受信するようにアプリを設定することもできます。しかし端末自体がロックされている時はアプリ自体が起動しないのでリアルタイムでの検知はできず、アプリのロックが解除されたタイミングでデータが反映されるようになります。なので起床したタイミングに即座に何かしらアクションを起こすといったことはできないようです。

参考記事:進化した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を使用します。変化が起きたタイミングでHKObserverQueryHKObserverQueryCompletionHandlerが実行されるのでその中で必要な処理を記述します。

/// 睡眠分析データを観測する
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を実行することでバックグラウンドでの取得を有効化することができます。

【Swift】HealthKitの導入方法と使い方!
// バックグランドでのヘルスケアデータの更新検知を有効にする
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

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index