简介
在HeathKit中,可以使用HKHealthStore类来访问健康App中的数据,如健身记录、营养摄入、睡眠状况等。也可以对这些数据进行读取和共享(即第三方App写入或删除数据到苹果健康App中)。笔者就以获取步数为例,还可以获取身高、体重、睡眠时间、心率等等。
使用前
在Xcode中, 打开HealthKit 功能
在info.plist文件中,增加NSHealthShareUsageDescription用于读取数据的描述和NSHealthUpdateUsageDescription用于写入数据的描述。
HeathKit不支持在iPad中使用,而且它也不支持扩展。
HeathKit框架
主要是抽象类HKObject + HKObjectType。
HKObject
在抽象类HKObject的子类中,每个对象都有下面的属性:
- UUID:对象的标识符
- source:数据的来源,来源可以是健康App,也可以第三方App。对象存储到
HeathKit中时会设置其来源。只有从HeathKit中获取到的数据的来源才有效。 - metadata:一个包含该对象额外信息的字典,元数据包含预定义的key和自定义的key,预定义的key用来帮助我们在应用间共享数据,而自定义的key用来扩展HeathKit,为对象添加针对应用的数据。
特征和样本
HeathKit的对象主要分为特征和样本(开始套娃):
- 特征:用户的基本不变的数据,包括用户的生日、血型和性别等。只能用户在健康App中添加或修改。
- 样本:某个时间段的数据,其对象都是
HKSample的子类,有以下属性:- type:样本类型,例如:步数、距离、心率等,其类型又可以分为以下四种:
HKCategorySample:类别样本,iOS 8 中,只有睡眠分析这一个类别样本。代表有限种类的样本。HKQuantitySample:代表存储数据的样本,比如步数、距离、用户的体温等。是最常见的数据类型。HKCorrelation:代表复合数据,包括一个或者多个样本。在iOS 8 中,用correlation代表食物和血压。在创建食物或血压时,需要用correlation。HKWorkout:代表某种活动,比如走、跑步等。包含有开始时间、结束时间、运动类型、消耗能量、运动距离等属性。还可以为workout关联许多详细的样本。不像correlation,这些样本不包含在workou中,但是可以通workout获取到。
- startDate:样本采样开始时间。
- endDate:样本采样结束时间。(存在startDate == endDate)
- type:样本类型,例如:步数、距离、心率等,其类型又可以分为以下四种:
常用的数据类型:
HKQuantityTypeIdentifierBodyMassIndex: 体重指数HKQuantityTypeIdentifierBodyFatPercentage: 体脂百分比HKQuantityTypeIdentifierHeight: 身高HKQuantityTypeIdentifierBodyMass: 体重HKQuantityTypeIdentifierLeanBodyMass: 瘦体重HKQuantityTypeIdentifierWaistCircumference: 腰围HKQuantityTypeIdentifierStepCount: 步数HKQuantityTypeIdentifierDistanceWalkingRunning: 步行+跑步距离HKQuantityTypeIdentifierFlightsClimbed: 上楼梯数HKQuantityTypeIdentifierDistanceSwimming: 游泳距离HKQuantityTypeIdentifierDistanceDownhillSnowSports: 滑雪距离HKQuantityTypeIdentifierHeartRate: 心率HKQuantityTypeIdentifierBodyTemperature: 身体温度HKQuantityTypeIdentifierBloodPressureSystolic: 血液收缩压HKQuantityTypeIdentifierBloodPressureDiastolic: 血液舒张压HKQuantityTypeIdentifierRespiratoryRate: 用户呼吸率HKQuantityTypeIdentifierRestingHeartRate: 静息心率HKQuantityTypeIdentifierWalkingHeartRateAverage: 步行时心率
一共有超过100项数据类型。目前常用或是能用也就这几项。
需要查询其他类型的数据只需替换类型即可。
HKUnit
读取的数据的单位的类,比如体重的单位kg,时间单位min(minutes)、hr(hours)、d(days)等。一般在获取数据或写入样本数据的时候需要添加对应的单位,如多少步、多少米等。
HKQuery
读取数据的方法,大致以下几类:
- HKHealthStore:提供了一个用于访问和存储用户健康数据的接口。
- HKSampleQuery:样本查询。这是使用最多的查询。使用样本查询可以查询在HeathKit中任意的数据。而且可以对结果进行排序等。
- HKObserverQuery:观察者查询的类:这是一个长时间运行的查询,它会检测HealthKit存储,并在匹配到的样本发生变化时通知你(可以后台)。
- HKAnchoredObjectQuery:锚定对象查询。用这种查询来搜索添加进存储的项。当锚定查询第一次执行时,会返回存储中所有匹配的样本。在接下来的执行中,只会返回上一次执行之后添加的项目。通常,锚定对象查询会和观察者查询一起使用。观察者查询告诉你某些项目发生了变化,而锚定对象查询来决定有哪些(如果有的话)项目被添加进了存储。
- HKStatisticsQuery:统计查询。使用这种查询来在一系列匹配的样本中执行统计运算。即计算与给定数量类型和谓词匹配的数量样本的统计信息。你可以使用统计查询来计算样本的总和、最小值、最大值或平均值。
- HKStatisticsCollectionQuery:统计集合查询。使用这种查询来在一系列长度固定的时间间隔中执行多次统计查询。通常使用这种查询来生成图表。查询提供了一些简单的方法来计算某些值,例如,每天消耗的总热量或者每5分钟行走的步数。统计集合查询是长时间运行的。查询可以返回当前的统计集合,也可以监测HealthKit存储,并对更新做出响应。
- HKCorrelation:Correlation查询。使用这种查询来在correlation查找数据。这种查询可以为correlation中每个样本类型包含独立的谓词。如果你只是想匹配correlation类型,那么请使用样本查询。
- HKSourceQuery:来源查询。使用这种查询来查找HealthKit存储中的匹配数据的来源(应用和设备)。来源查询会列出储存的特定样本类型的所有来源。
注意事项
因为是健康App的步数是可写入的,所以想要获取真实的步数,就需要将手动写入的数据过滤,即:真实步数 = 总步数 - 写入的步数。
获取步数完整代码
#import "ViewController.h"
#import <HealthKit/HealthKit.h>
@interface ViewController ()
// 创建healthStore实例对象
@property (nonatomic,strong) HKHealthStore *healthStore;
// 查询数据的类型,比如计步,行走+跑步距离等等
@property (nonatomic,strong) HKQuantityType *quantityType
// 谓词,用于限制查询返回结果
@property (nonatomic,strong) NSPredicate *predicate;
@end
@implementation ViewController
- (HKHealthStore *)healthStore{
if(_healthStore == nil){
_healthStore = [[HKHealthStore alloc]init];
}
return _healthStore;
}
- (HKQuantityType *)quantityType{
if(_quantityType == nil){
_quantityType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
}
return _quantityType;
}
- (NSPredicate *)predicate{
if(_predicate == nil){
// 构造当天时间段查询参数
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *now = [NSDate date];
// 开始时间
NSDate *startDate = [calendar startOfDayForDate:now];
// 结束时间
NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
_predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionStrictStartDate];
}
return _predicate;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self useHealthKit];
}
// 注意代码块中的循环引用,这里忽略
- (void)useHealthKit{
//判断设备是否支持查看healthKit数据
if([HKHealthStore isHealthDataAvailable] == NO){
NSLog(@"设备不支持healthKit");
return;
}
// 这里只获取运动步数的权限
NSSet *readObjectTypes = [NSSet setWithObjects:[HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount], nil];
// 向用户请求授权共享或读取健康App数据
[self.healthStore requestAuthorizationToShareTypes:nil readTypes:readObjectTypes completion:^(BOOL success, NSError * _Nullable error) {
if(success){
[self queryTotalStepCount:^(NSInteger stepCount) {
NSLog(@"真实运动步数(总步数 - 编辑的步数) = %ld",(long)stepCount);
}];
}else{
NSLog(@"获取步数权限失败");
}
}];
}
//这里的步数是总步数,和各个来源 包含手动编辑录入的
// HKStatisticsOptionCumulativeSum 总步数
// HKStatisticsOptionSeparateBySource 健康App所有步数数据的来源,包括iPhone、iWatch、健康App、第三方App等
- (void)queryTotalStepCount:(void(^)(NSInteger stepCount))completion{
HKStatisticsQuery *query = [[HKStatisticsQuery alloc]initWithQuantityType:self.quantityType quantitySamplePredicate:self.predicate options:HKStatisticsOptionCumulativeSum|HKStatisticsOptionSeparateBySource completionHandler:^(HKStatisticsQuery * _Nonnull query, HKStatistics * _Nullable result, NSError * _Nullable error) {
if (error) {
NSLog(@"获取失败!");
!completion?:completion(0);
return;
}
// 总步数
double totalStepCount = [result.sumQuantity doubleValueForUnit:[HKUnit countUnit]];
// 健康App编辑的步数
double userEnteredCount = 0;
// 遍历数据来源,获得健康App编辑的数值
for(HKSource *source in result.sources){
if([source.name isEqualToString:@"健康"]){
userEnteredCount = [[result sumQuantityForSource:source] doubleValueForUnit:[HKUnit countUnit]];
}
}
!completion?:completion((NSInteger)(totalStepCount - userEnteredCount));
}];
[self.healthStore executeQuery:query];
}
@end
正常情况下,过滤用户在健康App手动输入的步数后就能较精确的数值,但是第三方App在获取授权后也可以向健康App写入样本数据,如添加在某个开始时间到结束时间段内,行走了多少步的数据。
死磕?思考?
接下来我们简单深入下,我们依次做下以下操作:
- 我们用上面代码读取步数,比较健康App、微信、QQ、Keep、支付宝的数值
- 在健康App手动编辑输入10000步,再重复步骤一
- 使用代码写入10000步,再重复步骤一
在进行上述测试时,笔者的手机和手表放在桌面,保证不会有步数数据更新。
步骤一
我们使用HKSampleQuery查询下,获得每个样本更详细的数据:
// 结果排序,从开始到结束依次
NSSortDescriptor *startSortDec = [NSSortDescriptor sortDescriptorWithKey:HKPredicateKeyPathStartDate ascending:NO];
NSSortDescriptor *endSortDec = [NSSortDescriptor sortDescriptorWithKey:HKPredicateKeyPathEndDate ascending:NO];
HKSampleQuery *sampleQuery = [[HKSampleQuery alloc]initWithSampleType:self.quantityType predicate:self.predicate limit:HKObjectQueryNoLimit sortDescriptors:@[startSortDec,endSortDec] resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
if(error){
!completion?:completion(0);
return;
}else{
// 单位
HKUnit *unit = [HKUnit countUnit];
// 计算iPhone记录的步数
NSInteger iPhoneCount = 0;
// 计算iWatch记录的步数
NSInteger iWatchCount = 0;
// 计算健康App手动编辑的步数
NSInteger userEnteredCount = 0;
// 计算第三方App写入的步数
NSInteger thirdAppCount = 0;
// 遍历样本
for (HKQuantitySample *sample in results){
// 样本步数
NSInteger count = (NSInteger)[sample.quantity doubleValueForUnit:unit];
// 设备名称
NSString *deviceName = sample.device.name;
if (deviceName == nil) { // 包含手动编辑和第三方App写入
// 判断用户手动录入的数据。
NSInteger isUserEntered = [sample.metadata[HKMetadataKeyWasUserEntered] integerValue];;
if(isUserEntered == 1){
userEnteredCount += count;
}else{
thirdAppCount += count;
}
}else if ([deviceName isEqualToString:@"iPhone"]){
iPhoneCount += count;
}else if ([deviceName isEqualToString:@"Apple Watch"]){
iWatchCount += count;
}
}
NSLog(@"iPhone记录的步数 = %ld",(long)iPhoneCount);
NSLog(@"iWatch记录的步数 = %ld",(long)iWatchCount);
NSLog(@"健康App手动编辑的步数 = %ld",(long)userEnteredCount);
NSLog(@"第三方App写入的步数 = %ld",(long)thirdAppCount);
// 主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
!completion?:completion(userEnteredCount);
});
}
}];
[self.healthStore executeQuery:sampleQuery];
运行:
健康App截图:
微信截图:
QQ截图:
QQ特意分两种情况,授权使用健康App前后两种情况。 未授权截图:
授权后截图:
以上可知QQ获取运动步数的方式是,若用户授权使用健康App,则读取健康App的数据;若用户未授权则使用CMPedometer传感器获取。
Keep截图:
支付宝截图:
总结
多数App优先使用的还是健康App的数据,其次使用CMPedometer传感器获取运动步数。其中Keep作为一款主打运动的应用,可能有自己的优化或算法也算正常。
步骤二
在健康App手动编辑输入10000步:
代码读取:
健康App截图:
微信运动截图:
QQ运动截图:
Keep截图:
支付宝运动截图:
总结
在健康App手动输入10000步后,我们使用代码可以识别出手动编辑的,并计算真实的步数。除了Keep之外,其他的App操作均相同,过滤了手动编辑值的影响。
记住我们目前比较准确的步数是3288步。
步骤三
使用代码写入10000步,再重复步骤一
写入步数代码(也可以删除数据,这里就不演示了):
// 向健康App写入步数
- (void)userEnteredStepCount:(double)step{
// 授权写入的数据类型为步数
HKQuantityType *stepType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
NSSet *shareTypes = [NSSet setWithObjects:stepType, nil];
[self.healthStore requestAuthorizationToShareTypes:shareTypes readTypes:nil completion:^(BOOL success, NSError * _Nullable error) {
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *now = [NSDate date];
// 开始时间,这里为当天的零点时刻
NSDate *startDate = [calendar startOfDayForDate:now];
// 多少步
HKQuantity *stepQuantity = [HKQuantity quantityWithUnit:[HKUnit countUnit] doubleValue:step];
// 创建样本(某个时间段或时刻内,行走多少步)
HKQuantitySample *stepSample = [HKQuantitySample quantitySampleWithType:stepType quantity:stepQuantity startDate:startDate endDate:[NSDate date]];
// 写入样本
[self.healthStore saveObject:stepSample withCompletion:^(BOOL success, NSError * _Nullable error) {
if (error) {
NSLog(@"error: %@", error.localizedDescription);
}
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"写入%@",success ? @"成功" : @"失败");
});
}];
}];
}
代码读取:
健康App:
微信运动:
QQ运动:
Keep运动:
支付宝运动:
总结
还记得我们正常的步数是3288吗!
代码写入10000步后,各个App差异就比较大了。我们可以在设置中查看哪个第三方App写入了数据:
其次查询HKSource数据来源,也可以知道当前数据来源一共有四种:
首先是我们代码获取的,这是可以预见的,毕竟我们只是过滤了用户在健康App手动输入的,对于第三方App写入的数据并没有处理。
健康App也并不是我们想象的那样直接将与之前的数值相加,而是做了一定的处理,这也是可以预见的,在第三方写入的时候,在不同的时间段的重复数据进行优化!
其中QQ运动也飘了,而且奇怪的是再删除了步骤一和步骤二添加的20000步时,其他的App或多或少都对应更新数据,但QQ好像是将数据本地持久化了,一直不变;而且笔者测试了多次,有时候QQ的算法跟我们代码实现的数值一致,即总步数 - 编辑的步数,但有的时候却又不一样。不知道是否是刷新的问题?但可以肯定的是QQ的运动步数存在问题。
而Keep依旧,并没有过滤手动输入和第三方写入的数据。
微信和支付宝还是稳啊!依然是3288。
我们也可以用代码输出所有的样本信息,因为篇幅原因,我们取用前后的截图:
由此我们可以准确知道在查询时间内每个样本的采样时间、来源、数值、方式等。
也可以知道为什么第三方写入步数数据后,健康App没有直接相加的原因。因为我们写入的时间段是:00:00:00 ~ 13:54:36,这个时间段中,iPhone和iWatch都有运动的记录,三者数据或多或少有一定的交叉,甚至是手机和手表在相同的时间段中的步数也有一定的差异,健康App应该做了一定的优化和算法处理,得到他认为合理的数值。
而微信和支付宝则直接摒弃了所有外来的数据。哪一种处理方式合理,其实我们实验结果已经很明了了。
不足之处
这点笔者很遗憾,研究了一个下午的时候也没有弄清微信的优化和算法。有知道的大佬还请不吝赐教,感谢!
最后附录步数数据:
// 时间段数据(设备 开始时间 结束时间 步数)
iPhone 08:15:47 08:15:49 4
iPhone 08:15:21 08:15:24 5
Apple Watch 08:11:43 08:15:10 265
iPhone 08:07:55 08:15:21 786
iPhone 08:07:08 08:07:55 9
Apple Watch 08:04:02 08:11:43 584
iPhone 07:56:55 08:04:05 194
Apple Watch 07:52:31 07:59:13 254
iPhone 07:46:04 07:55:40 83
iPhone 07:22:51 07:22:56 12
Apple Watch 运动时间 = 1069.82 s
iPhone 运动时间 = 1509.27 s
运动总时间 = 1074.98 s
真实运动步数(总步数 - 编辑的步数) = 1115
健康App手动编辑的步数 = 0
第三方App写入的步数 = 0