MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的Health App数据集成

2023-02-057.1k 阅读

1. 了解 Health App 与 HealthKit 框架

在 iOS 生态系统中,Health App 是用户管理个人健康数据的核心应用。它可以收集来自各种健康和健身应用以及兼容设备的数据。Objective - C 开发者可以利用 HealthKit 框架与 Health App 进行交互,实现数据的读取、写入和共享。

HealthKit 提供了一个统一的健康数据模型,涵盖了各种健康相关的数据类型,例如步数、心率、睡眠数据等。每个数据类型都由 HKObjectType 类的实例表示。在与 Health App 集成之前,首先要了解 HealthKit 中不同的数据类型以及如何表示它们。

1.1 常见的 HealthKit 数据类型

  • 步数:使用 HKQuantityTypeIdentifierStepCount 来表示。步数数据属于数量类型(HKQuantityType),它记录用户在一段时间内行走的步数。
  • 心率:通过 HKQuantityTypeIdentifierHeartRate 标识。心率数据也是数量类型,通常以每分钟心跳次数(bpm)为单位。
  • 睡眠分析:睡眠数据稍微复杂一些,使用 HKCategoryTypeIdentifierSleepAnalysis。它是一个分类类型,用于记录用户的睡眠状态,例如睡眠、清醒等。

2. 配置项目以使用 HealthKit

在 Objective - C 项目中使用 HealthKit,需要进行一些配置步骤。

2.1 添加 HealthKit 框架

首先,在 Xcode 项目中添加 HealthKit 框架。打开项目导航器,选择项目设置,然后在“General”标签下的“Frameworks, Libraries, and Embedded Content”部分,点击“+”按钮,搜索并添加“HealthKit.framework”。

2.2 请求授权

在访问 Health App 中的数据之前,应用必须请求用户授权。授权过程分为读取数据权限和写入数据权限。下面是一个请求步数读取权限和睡眠写入权限的示例代码:

#import <HealthKit/HealthKit.h>

// 假设这是在视图控制器中
@interface ViewController () <HKHealthStoreDelegate>

@property (strong, nonatomic) HKHealthStore *healthStore;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.healthStore = [[HKHealthStore alloc] init];
    self.healthStore.delegate = self;
    
    // 定义要读取的数据类型
    NSSet *readTypes = [NSSet setWithObjects:
                        [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount],
                        nil];
    
    // 定义要写入的数据类型
    NSSet *writeTypes = [NSSet setWithObjects:
                         [HKObjectType categoryTypeForIdentifier:HKCategoryTypeIdentifierSleepAnalysis],
                         nil];
    
    [self.healthStore requestAuthorizationToShareTypes:writeTypes readTypes:readTypes completion:^(BOOL success, NSError * _Nullable error) {
        if (!success || error) {
            NSLog(@"授权失败: %@", error);
        } else {
            NSLog(@"授权成功");
        }
    }];
}

@end

3. 从 Health App 读取数据

一旦获得授权,就可以从 Health App 中读取数据。读取数据通常涉及指定数据类型、时间范围以及查询选项。

3.1 读取步数数据

下面的代码示例展示了如何读取过去一周的步数数据:

- (void)readStepCountForLastWeek {
    HKQuantityType *stepCountType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
    
    // 设置时间范围为过去一周
    NSDate *now = [NSDate date];
    NSDateComponents *components = [[NSDateComponents alloc] init];
    components.day = -7;
    NSDate *startDate = [[NSCalendar currentCalendar] dateByAddingComponents:components toDate:now options:0];
    
    HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:stepCountType predicate:[HKQuery predicateForSamplesWithStartDate:startDate endDate:now options:HKQueryOptionNone] limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
        if (error) {
            NSLog(@"读取步数数据错误: %@", error);
        } else {
            HKQuantity *totalSteps = [HKQuantity quantityWithUnit:[HKUnit countUnit] doubleValue:0];
            for (HKQuantitySample *sample in results) {
                totalSteps = [totalSteps addQuantity:sample.quantity];
            }
            NSLog(@"过去一周的总步数: %@", totalSteps);
        }
    }];
    
    [self.healthStore executeQuery:query];
}

3.2 读取心率数据

读取心率数据的过程类似,但心率数据可能在不同的时间点记录,并且可能有不同的采样频率。以下代码读取过去一小时的心率数据:

- (void)readHeartRateForLastHour {
    HKQuantityType *heartRateType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeartRate];
    
    NSDate *now = [NSDate date];
    NSDateComponents *components = [[NSDateComponents alloc] init];
    components.hour = -1;
    NSDate *startDate = [[NSCalendar currentCalendar] dateByAddingComponents:components toDate:now options:0];
    
    HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:heartRateType predicate:[HKQuery predicateForSamplesWithStartDate:startDate endDate:now options:HKQueryOptionNone] limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
        if (error) {
            NSLog(@"读取心率数据错误: %@", error);
        } else {
            for (HKQuantitySample *sample in results) {
                HKQuantity *heartRate = sample.quantity;
                double heartRateValue = [heartRate doubleValueForUnit:[HKUnit countUnitWithMetricPrefix:HKMetricPrefixKilo unit:HKUnit minuteUnit]];
                NSLog(@"心率: %.2f bpm, 时间: %@", heartRateValue, sample.startDate);
            }
        }
    }];
    
    [self.healthStore executeQuery:query];
}

4. 向 Health App 写入数据

除了读取数据,应用也可以向 Health App 写入数据。这对于记录用户的健康活动或者同步来自外部设备的数据非常有用。

4.1 写入睡眠数据

假设应用通过自身的算法分析出用户的睡眠状态,以下代码展示了如何将睡眠数据写入 Health App:

- (void)writeSleepData:(HKCategoryValueSleepAnalysis)sleepState startDate:(NSDate *)startDate endDate:(NSDate *)endDate {
    HKCategoryType *sleepType = [HKCategoryType categoryTypeForIdentifier:HKCategoryTypeIdentifierSleepAnalysis];
    HKCategorySample *sleepSample = [HKCategorySample categorySampleWithType:sleepType value:sleepState startDate:startDate endDate:endDate];
    
    [self.healthStore saveObject:sleepSample withCompletion:^(BOOL success, NSError * _Nullable error) {
        if (!success || error) {
            NSLog(@"写入睡眠数据失败: %@", error);
        } else {
            NSLog(@"睡眠数据写入成功");
        }
    }];
}

这里 HKCategoryValueSleepAnalysis 是一个枚举类型,定义了不同的睡眠状态,例如 HKCategoryValueSleepAnalysisAsleep 表示睡眠状态,HKCategoryValueSleepAnalysisAwake 表示清醒状态。

4.2 写入自定义健康数据

有时候,应用可能需要记录 HealthKit 标准数据类型之外的自定义健康数据。这需要先定义一个自定义的 HKQuantityType

首先,在应用的 Info.plist 文件中添加一个新的键 NSHealthKitCustomObjectTypes,它是一个数组,用于列出所有自定义的数据类型。每个数组元素是一个字典,包含 HKQuantityTypeIdentifier(自定义标识符)和 HKQuantityTypeLocalizedDisplayName(本地化显示名称)。

然后,在代码中定义和使用自定义数据类型:

// 假设自定义数据类型表示某种运动强度指标
HKQuantityType *customExerciseIntensityType = [HKQuantityType quantityTypeForIdentifier:@"com.example.app.exerciseIntensity"];
if (!customExerciseIntensityType) {
    HKUnit *intensityUnit = [HKUnit countUnit];
    customExerciseIntensityType = [HKQuantityType quantityTypeWithIdentifier:@"com.example.app.exerciseIntensity" unit:intensityUnit];
}

HKQuantity *intensityQuantity = [HKQuantity quantityWithUnit:intensityUnit doubleValue:5];
NSDate *now = [NSDate date];
HKQuantitySample *intensitySample = [HKQuantitySample quantitySampleWithType:customExerciseIntensityType quantity:intensityQuantity startDate:now endDate:now];

[self.healthStore saveObject:intensitySample withCompletion:^(BOOL success, NSError * _Nullable error) {
    if (!success || error) {
        NSLog(@"写入自定义运动强度数据失败: %@", error);
    } else {
        NSLog(@"自定义运动强度数据写入成功");
    }
}];

5. 处理 Health App 数据变更通知

Health App 中的数据可能会因为其他应用的写入或者设备数据更新而发生变化。应用可以注册监听这些数据变更通知,以便及时更新自身的显示或者重新计算相关数据。

5.1 注册数据变更通知

通过 HKObserverQuery 来注册数据变更通知。以下代码示例监听步数数据的变化:

- (void)registerStepCountChangeNotification {
    HKQuantityType *stepCountType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
    
    HKObserverQuery *query = [[HKObserverQuery alloc] initWithSampleType:stepCountType predicate:nil updateHandler:^(HKObserverQuery * _Nonnull query, HKObserverQueryCompletionHandler  _Nonnull completionHandler, NSError * _Nullable error) {
        if (error) {
            NSLog(@"注册步数数据变更通知错误: %@", error);
        } else {
            NSLog(@"步数数据发生变化");
            completionHandler(HKObserverQueryNoData);
        }
    }];
    
    [self.healthStore executeQuery:query];
}

5.2 处理数据变更

当接收到数据变更通知后,通常需要重新读取相关数据以获取最新的值。例如,在接收到步数数据变更通知后,可以重新执行读取步数数据的方法:

- (void)handleStepCountChange {
    [self readStepCountForLastWeek];
}

6. 与 Health App 集成的最佳实践

在与 Health App 集成时,遵循一些最佳实践可以提高应用的稳定性、用户体验和数据安全性。

6.1 尊重用户隐私

始终明确告知用户应用会访问和使用 Health App 中的哪些数据,并且在获取授权时确保用户清楚了解这些权限。只有在必要时才请求写入权限,并且对写入的数据进行严格的验证和管理,避免误操作导致用户数据混乱。

6.2 数据验证与一致性

在读取和写入数据时,要对数据进行严格的验证。例如,步数数据应该是一个非负整数,心率数据应该在合理的范围内(通常成年人静息心率在 60 - 100 bpm 之间)。当写入数据时,确保数据与 HealthKit 的数据模型一致,避免因为数据格式问题导致同步失败。

6.3 错误处理

在与 HealthKit 交互的过程中,可能会遇到各种错误,例如授权失败、查询失败、保存失败等。应用应该对这些错误进行适当的处理,向用户提供友好的错误提示,并且记录错误日志以便调试和改进。

6.4 性能优化

Health App 可能包含大量的数据,尤其是对于长期使用 HealthKit 的用户。在进行数据查询时,尽量缩小时间范围和限制返回的数据量,以提高查询性能。避免在主线程中执行耗时的 HealthKit 操作,防止应用界面卡顿。

7. 处理 HealthKit 中的复杂数据关系

HealthKit 中的一些数据类型存在复杂的关系,例如活动数据可能与步数、心率等数据相关联。理解和处理这些关系可以为用户提供更全面的健康分析。

7.1 关联活动与步数数据

假设应用记录了用户的跑步活动,并且想将跑步期间的步数关联起来。可以通过在活动记录中添加步数相关的元数据来实现:

// 假设已经有一个跑步活动记录(HKWorkout)
HKWorkout *runningWorkout = // 获取到的跑步活动记录

// 获取跑步期间的步数数据
HKQuantityType *stepCountType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
NSDate *startDate = runningWorkout.startDate;
NSDate *endDate = runningWorkout.endDate;
HKSampleQuery *stepCountQuery = [[HKSampleQuery alloc] initWithSampleType:stepCountType predicate:[HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionNone] limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
    if (!error && results.count > 0) {
        HKQuantity *totalSteps = [HKQuantity quantityWithUnit:[HKUnit countUnit] doubleValue:0];
        for (HKQuantitySample *sample in results) {
            totalSteps = [totalSteps addQuantity:sample.quantity];
        }
        
        // 将步数作为元数据添加到跑步活动记录中
        NSMutableDictionary *metadata = [runningWorkout.metadata mutableCopy];
        [metadata setObject:totalSteps forKey:@"com.example.app.runStepCount"];
        runningWorkout.metadata = metadata;
        
        // 保存更新后的活动记录
        [self.healthStore saveObject:runningWorkout withCompletion:^(BOOL success, NSError * _Nullable error) {
            if (!success || error) {
                NSLog(@"保存带有步数元数据的跑步活动失败: %@", error);
            } else {
                NSLog(@"带有步数元数据的跑步活动保存成功");
            }
        }];
    }
}];

[self.healthStore executeQuery:stepCountQuery];

7.2 分析睡眠与心率的关系

睡眠数据和心率数据在时间上可能存在关联。可以通过查询一段时间内的睡眠数据和心率数据,并进行数据分析来找出这种关系。例如,分析睡眠期间的平均心率:

- (void)analyzeSleepAndHeartRate {
    // 获取睡眠数据
    HKCategoryType *sleepType = [HKCategoryType categoryTypeForIdentifier:HKCategoryTypeIdentifierSleepAnalysis];
    NSDate *startDate = // 开始时间
    NSDate *endDate = // 结束时间
    HKSampleQuery *sleepQuery = [[HKSampleQuery alloc] initWithSampleType:sleepType predicate:[HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionNone] limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
        if (!error && results.count > 0) {
            for (HKCategorySample *sleepSample in results) {
                if (sleepSample.value == HKCategoryValueSleepAnalysisAsleep) {
                    NSDate *sleepStart = sleepSample.startDate;
                    NSDate *sleepEnd = sleepSample.endDate;
                    
                    // 获取睡眠期间的心率数据
                    HKQuantityType *heartRateType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeartRate];
                    HKSampleQuery *heartRateQuery = [[HKSampleQuery alloc] initWithSampleType:heartRateType predicate:[HKQuery predicateForSamplesWithStartDate:sleepStart endDate:sleepEnd options:HKQueryOptionNone] limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable heartRateResults, NSError * _Nullable heartRateError) {
                        if (!heartRateError && heartRateResults.count > 0) {
                            HKQuantity *totalHeartRate = [HKQuantity quantityWithUnit:[HKUnit countUnitWithMetricPrefix:HKMetricPrefixKilo unit:HKUnit minuteUnit] doubleValue:0];
                            for (HKQuantitySample *heartRateSample in heartRateResults) {
                                totalHeartRate = [totalHeartRate addQuantity:heartRateSample.quantity];
                            }
                            double averageHeartRate = [totalHeartRate doubleValueForUnit:[HKUnit countUnitWithMetricPrefix:HKMetricPrefixKilo unit:HKUnit minuteUnit]] / heartRateResults.count;
                            NSLog(@"睡眠期间平均心率: %.2f bpm", averageHeartRate);
                        }
                    }];
                    [self.healthStore executeQuery:heartRateQuery];
                }
            }
        }
    }];
    
    [self.healthStore executeQuery:sleepQuery];
}

8. 应对 HealthKit 版本变化

随着 iOS 系统的更新,HealthKit 框架也可能会有版本变化,新增数据类型、修改 API 等。应用开发者需要及时关注这些变化,以确保应用的兼容性。

8.1 检查 HealthKit 版本

可以通过检查系统版本来判断 HealthKit 的版本。例如,在 iOS 14 中,HealthKit 可能有新的功能和数据类型:

if (@available(iOS 14.0, *)) {
    // 处理 iOS 14 及以上版本的 HealthKit 新特性
} else {
    // 处理旧版本 HealthKit 的兼容逻辑
}

8.2 更新数据处理逻辑

当 HealthKit 有新的数据类型或者 API 变化时,需要更新应用的数据读取、写入和处理逻辑。例如,如果新的 HealthKit 版本引入了一种新的压力监测数据类型,应用需要添加相应的授权请求、数据读取和处理代码。

// 在 iOS 新系统版本中假设新增了压力监测数据类型
if (@available(iOS 15.0, *)) {
    HKQuantityType *stressLevelType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStressLevel];
    if (stressLevelType) {
        // 请求读取压力监测数据的权限
        NSSet *readTypes = [NSSet setWithObject:stressLevelType];
        [self.healthStore requestAuthorizationToShareTypes:nil readTypes:readTypes completion:^(BOOL success, NSError * _Nullable error) {
            if (success) {
                // 读取压力监测数据
                NSDate *now = [NSDate date];
                NSDateComponents *components = [[NSDateComponents alloc] init];
                components.hour = -1;
                NSDate *startDate = [[NSCalendar currentCalendar] dateByAddingComponents:components toDate:now options:0];
                
                HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:stressLevelType predicate:[HKQuery predicateForSamplesWithStartDate:startDate endDate:now options:HKQueryOptionNone] limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
                    if (!error && results.count > 0) {
                        for (HKQuantitySample *sample in results) {
                            HKQuantity *stressLevel = sample.quantity;
                            double stressValue = [stressLevel doubleValueForUnit:[HKUnit countUnit]];
                            NSLog(@"压力水平: %.2f, 时间: %@", stressValue, sample.startDate);
                        }
                    }
                }];
                
                [self.healthStore executeQuery:query];
            }
        }];
    }
}

通过以上步骤和方法,Objective - C 开发者可以全面地与 Health App 进行数据集成,为用户提供更丰富、更个性化的健康管理功能。在实际开发中,还需要根据应用的具体需求和场景,灵活运用 HealthKit 的各种特性和 API。