コンテンツにスキップ

HealthKit実装メモ

iOSアプリでHealthKitを利用する際のメモ。 初期設定を中心に記載する。

XCodeのバージョンは15.0.1を利用した。

プロジェクトの設定

プロジェクトごとにXCodeで設定が必要なため、以下の設定を行う。

  • アプリ設定のSigning & CapabilitiesHealthKitを追加する
  • InfoCustom iOS Target Propertiesにヘルスケアデータの利用文を設定する
    • 読み込み:Privacy - Health Share Usage Description
    • 書き込み:Privacy - Health Update Usage Description

ヘルスケアデータの初期化

今回はテストを簡単にしたいため、Serviceクラスを作成して、HealthKitのデータを取得するようにする。

ヘルスケアデータを利用する際はHKUnitと呼ばれる計測値の単位が必要なため、取得したいデータの単位を設定する。 また、サービス開始時にユーザーに許可を求めるため、requestAuthorizationを利用する。

Swift
class HealthService: ObservableObject {

    let healthStore: HKHealthStore
    let metsUnit = HKUnit.kilocalorie().unitDivided(by: HKUnit.gramUnit(with: .kilo)).unitDivided(by: HKUnit.hour())

    init() {
        self.healthStore = HKHealthStore()
        let mets = HKQuantityType(.physicalEffort)
        let healthTypes: Set = [mets]

        Task {
            do {
                try await self.healthStore.requestAuthorization(toShare: [], read: healthTypes)
            } catch {
                print("failed to authorization.")
            }
        }
    }
}

作成したサービスは各Viewの中でEnvironmentObjectとして利用する。 直接インスタンス化するとプレビューでクラッシュしてしまうが、適切にモックに差し替えることで、クラッシュせずにプレビューができる。

インスタンス化は@mainで行う。

Swift
1
2
3
4
5
6
7
8
9
@main
struct HealthApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(HealthService())
        }
    }
}

Viewの中で利用する際は以下のように記述する。

Swift
1
2
3
struct HealthView: View {
    @EnvironmentObject var healthService: HealthService
}

データの取得

データの取得にはHealthKitのクエリを利用する。

Swift
    func getRawMets(date: Date = Date()) async -> [MetsRawData] {
        let pyhsicalEffortType = HKQuantityType(.physicalEffort)
        let startDay = date.diff(days: 0)
        let tomorrow = date.diff(days: 1)

        // Create the predicate for the query
        let predicate = HKQuery.predicateForSamples(withStart: startDay, end: tomorrow, options: .strictStartDate)
        return await withCheckedContinuation{continuation in
            let query = HKSampleQuery(sampleType: pyhsicalEffortType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { (query, samples, error) in
                if let error = error {
                    print("Error: \(error.localizedDescription)")
                    continuation.resume(returning: [])
                } else {
                    guard let quantitySamples = samples as? [HKQuantitySample] else {
                        print("No data available")
                        return continuation.resume(returning: [])
                    }
                    var ret: [MetsRawData] = []
                    // Process the samples

                    for sample in quantitySamples {
                        let mets = sample.quantity.doubleValue(for: self.metsUnit)
                        if (3 < mets) {
                            let second = sample.endDate.timeIntervalSince(sample.startDate)
                            let methHour = mets * second / 3600
                            ret.append(MetsRawData(startDate: sample.startDate, endDate: sample.endDate, mets: methHour))
                        }
                    }
                    continuation.resume(returning: ret)
                }
            }
            // Execute the query
            healthStore.execute(query)

        }
    }

データの書き込み

TODO: あとで書く

プレビューでのモック差し替え

XCodeのプレビューではヘルスケアデータが取得できないため、アプリがクラッシュする。 そのため、プレビュー時にはHealthKitを利用しないモックデータを差し込むようにする。

モックは作成したHealthServiceを継承して、メソッドをオーバーライドする。

Swift
1
2
3
4
5
6
7
class HealthServiceMock: HealthService {
    override init() {}

    override func getRawMets(date: Date) async -> [MetsRawData] {
        return []
    }
}

作成したモックはプレビューでは以下のように注入すればよい。

Swift
1
2
3
4
#Preview {
    WeekView()
        .environmentObject(HealthServiceMock() as HealthService)
}