Blog by Frank

HealthKit

HealthKit 概览

关于 HealthKit 框架

Demo App

graph LR;
    NSObject --> HKObject;
    NSObject --> HKObjectType;

    HKObject --> HKSample;
    HKSample --> HKQuantitySample-身高心率卡路里等数量数据;
    HKSample --> HKCategorySample-躺在床上/熟睡/醒着等分类数据;
    HKSample --> HKWorkout-储存一次活动,例如骑行;
    HKSample --> HKCorrelation跟食物和血压数据等相关性数据;

    HKObjectType --> HKSampleType
    HKSampleType --> HKQuantityType;
    HKSampleType --> HKCategoryType;
    HKSampleType --> HKWorkoutType;
    HKSampleType --> HKCorrelationType;

    Parent --> Children;

Create and save health and fitness samples.

HealthKit Store(HealthKit 商店) :

Tell if a Health Kit sample came from an Apple Watch?

HKSource

sample data

HKObject 的属性

HKSample 的属性

Samples 可以进一步分为四个子类

HealthKitProvider 类,用于和 HealthKit 交互

Demo App

//
//  HealthKitProvider.swift
//  Bike Log
//
//  Created by Ameir Al-Zoubi on 1/13/22.
//

import Foundation
import HealthKit
import Combine

class HealthKitProvider: HealthProvider {
    
    let store = HKHealthStore()
    let publisher = CurrentValueSubject<HealthResult, Never>(.empty)
    var updateTask: Task<(), Error>?
    
    let healthTypes: Set = [HKWorkoutType.workoutType(),
                            HKQuantityType.quantityType(forIdentifier: .distanceCycling)!,
                            HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!,
                            HKQuantityType.quantityType(forIdentifier: .heartRate)!,
                            HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!,
                            HKQuantityType.quantityType(forIdentifier: .basalEnergyBurned)!]
    
    func requestAuthorization() async {
        do {
            try await store.requestAuthorization(toShare: [], read: healthTypes)
        } catch {
            print("Error requesting HealthKit authorization \(error)")
        }
    }

    func queryWorkouts(activity: Activity) {
        updateTask?.cancel()

        let activityPredicate = HKSamplePredicate.workout(HKQuery.predicateForWorkouts(with: activity.hkActivityType))
        let query = HKAnchoredObjectQueryDescriptor(predicates: [activityPredicate], anchor: publisher.value.anchor)
        let updatingResults = query.results(for: store)

        updateTask = Task {
            for try await newResult in updatingResults {
                publishResult(newResult, activity: activity)
            }
        }
    }
    
    private func publishResult(_ result: HKAnchoredObjectQueryDescriptor<HKWorkout>.Result, activity: Activity) {
        let unit = publisher.value.unit
        let newWorkouts = result.addedSamples.compactMap { Workout(hkWorkout: $0, hkUnit: unit.hkUnit()) }
        let deletedIDs = result.deletedObjects.map { $0.uuid }
        let validWorkouts = publisher.value.workouts.filter { !deletedIDs.contains($0.id) }
        let workouts = Array(Set(validWorkouts + newWorkouts))
        
        let newState = HealthResult(workouts: workouts,
                                    unit: publisher.value.unit,
                                    cached: false,
                                    anchor: result.newAnchor)
        publisher.value = newState
        newState.save(for: activity)
    }
    
    func preferredUnit(for type: HKQuantityType) async -> HKUnit {
        do {
            let units = try await store.preferredUnits(for: [type])
            if let unit = units[type] {
                return unit
            }
        } catch {
            print("Error retrieving preferredUnits \(error)")
        }
        
        return Locale.current.distanceUnit().hkUnit()
    }
    
    func monitorWorkouts(activity: Activity) async {
        let cachedResult = try? await HealthResult.load(activity: activity)
        publisher.value = cachedResult ?? .empty

        await requestAuthorization()
        let hkUnit = await preferredUnit(for: HKWorkout.distanceType(for: activity.hkActivityType))
        let unit = hkUnit.lengthUnit()

        if unit != publisher.value.unit {
            let workouts = Workout.updateWorkouts(publisher.value.workouts, unit: unit)
            let updatedResult = HealthResult(workouts: workouts, unit: unit, cached: true, anchor: cachedResult?.anchor)
            publisher.value = updatedResult
        }
        
        queryWorkouts(activity: activity)
    }
    
    func workoutsPublisher() -> AnyPublisher<HealthResult, Never> {
        publisher
            .receive(on: RunLoop.main)
            .eraseToAnyPublisher()
    }
}