iOS 8 HealthKit 介绍

October 30, 2014

如何创建 HealthKit 数据

在创建数据前,我们首先需要知道 HealthKit 的数据结构。

HKUnit

是基本的数据单位,它下面有很多子类表示不同的单位。

HKQuantity

表示某一种数据单位的数量,可以通过这个对象获取一种单位转换成另一种单位的值:

1
2
3
4
HKUnit *gramUnit = [HKUnit gramUnit];
HKQuantity *grams = [HKQuantity quantityWithUnit:gramUnit doubleValue:20];
double kg = [grams doubleValueForUnit:[HKUnit unitFromString:@"kg"]];
// kg -> 0.2

但是前提是这两个互相转换的单位之间是理论上可以转换的,可以通过 HKUnit 的实例方法 - (BOOL)isCompatibleWithUnit: (HKUnit *)unit; 来判断两个单位之间是否可以转换

HKObjectType

用来表示一个数据是什么类型的,HealthKit 中有几十种类型。HKObjectType 的层级结构可以看下图:

图1 HKObjectType 的层级结构

图1 HKObjectType 的层级结构

在 HealthKit 中我们不能创建自己的类型,但是理解了 HKObjectType 的数据结构对我们今后的开发有很大好处的。 HKObjectType 对象中主要有两个属性:

  • identifier
  • type name

我们可以根据需要创建不同类型的 HKObjectType:

1
2
3
4
5
 @interface HKobjectType : NSObject
  + (HKQuantityType *)quantityTypeForIdentifier:(NSString *)identifier;
  + (HKCategoryType *)categoryTypeForIdentifier:(NSString *)identifier;
  + (HKCharacteristicType *)characteristicTypeForIdentifier:(NSString *)identifier;
 @end

HKObject

所有存储在 HealthKit 中的数据都是 HKObject 的子类。HKObject 的类结构和 HKObjectType 的类结构很相似

图2 HKObject 类的层级结构

图2 HKObject 类的层级结构

HKQuantitySample

是目前 HealthKit 中使用最广泛的一个 HKObject。它主要有两个属性:

1
2
3
4
 @interface HKQuantitySample
  @property (readonly) HKQuantityType *quantityType;  // 表示这个数量是什么类型的
  @property (readonly) HKQuantity *quantity; // 数量的值
 @end

需要注意的是,quantityTypequantity 必须是正确匹配的否则在运行时程序会抛出异常。

HKCategorySample

它和 HKQuantitySample 类似

1
2
3
4
@interface HKCategorySample
    @property (readonly) HKCategoryType *categoryType;
    @property (readonly) NSInteger value;
@end

HKSample

1
2
3
4
5
@interface HKSample
    @property (readonly) HKSampleType *sampleType; // 会依据子类来确定这个类型的最终值
    @property (readonly) NSDate *startDate;
    @property (readonly) NSDate *endDate;
@end

HKObject

1
2
3
4
5
@interface HKObject
    @property (readonly) NSUUID *UUID; // 唯一识别
    @property (readonly) HKSource *source; // 数据来源
    @property (readonly) NSDictionary *metadata; // 其它数据
@end

HKObject 中的所有属性都是不可变的,因为一旦数据收集后就不应该被更改。HKObject 也是不可变的。所以想要创建一个 HKObject 数据我们应该通过 HKQuantitySample 来创建

创建一个 HKObject

1
2
3
4
5
6
7
8
9
10
11
12
13
NSString *identifier = HKQuantityTypeIdentifierBodyTemperature;
HKQuantityType *tempType = [HKObjectType quantityTypeForIdentifier:identifier];

HKQuantity *myTemp = [HKQuantity quantityWithUnit:[HKUnit degreeFahrenheitUnit] doubleValue:98.6];

NSDictionary *meta = @{
    HKMetadataKeyBodyTemperatureSensorLocation: @(HKBodyTemperatureSensorLocationEar)
}
HKQuantitySample *temperatureSample = [HKQuantitySample quantitySampleWithType:tempType
                    quantity:myTemp
                    startDate:[NSDate date]
                    endDate: [NSDate date]
                    metadata: meta];

通过上面的代码,我们已经创建了一个新的数据,接下来我们要将这个数据保存到 HealthKit 的数据库中

保存 HealthKit 数据

HKHealthStore

可以想象成我们通过这个对象与 HealthKit 的数据库进行连接,可以通过这个对象保存和查询数据。需要注意的是,HKHealthStore 在应用中应该只保存一个对象,因为每次创建新的对象,实际上相当于是同一个对象,可以理解为它们都会链接到同一个数据库。

使用 HKHealthStore 保存数据的例子:

1
2
3
4
5
6
7
8
self.store = [HKHealthStore new];
...
HKQuantitySample *mySample = [self newSample];
[self.store saveObject:mySample withCompletion:^(BOOL success, NSError *error) {
    if (success) {
        NSLog(@"Object Saved!")
    }
}];

保存数据的代码看起来很容易理解,成功保存数据后,接下来我们希望能从 HealthKit 中获取我们想要的数据

获取 HealthKit 数据

可以获取的数据除了 HKObject 外还有 Characteristics

Characteristics

这个数据是用户保存在 HealthKit 中个人信息,这些信息一般情况下不会改变。比如:生日日期、血型、性别等 通过 API 获取这些信息很简单,下面代码用来获取用户记录在 healthKit 中的生日

1
2
NSError *error;
NSDate *dateOfBirth = [self.store dateOfBirthWithError:&error];

但是大部分数据并不能用这么简单的方式获取,这时候我们就需要通过像条件查询一样的东西来查询数据

HKQuery

我们可以使用 Predicates

1
2
HKQuantity *weight = ...
[NSPredicate predicateWithFormat:@"%k > %@", HKPredicateKeyPathQuantity, weight];

为了简化 NSPredicate 的使用,HealthKit 中提供了 NSPredicateOperatorType,我们可以将这个 Enum 传入 HKQuery 的一个工厂方法中,生成想要的 HKQuery 对象。

1
2
NSPredicateOperatorType greaterThan = NSGreaterThanPredicateOperatorType;
[HKQuery predicateForQuantitySamplesWithOperatorType:greaterThan quantity:weight];

HKSampleQuery

  • 可以设置限制: HKObjectQueryNoLimit
  • 排序:NSSortDescriptors

一个获取最近血糖值的例子

1
2
3
4
5
6
7
8
9
10
11
HKQuantityType *bloodSugar = ...
NSString *endKey = HKSampleSortIdentifierEndDate;
NSSortDescriptor *endDate = [NSSortDescriptor sortDescriptorWithKey:endKey ascending:NO];
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:bloodSugar
                predicate:nil
                limit:1
                sortDescriptors:@[endDate]
                resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
    HKQuantitySample *sample = [results lastObject];
    NSLog(@"Sample:  %@", sample);
}];

通过上面的代码可以获取到指定的信息,但是如果我们想要实时更新数据,在每次数据改变时都会获取到最新的数据,我们可以使用 HKObserverQuery

HKObserverQuery

用于监听 HealthKit 数据库中的改变。 它和 HKSampleQuery 的不同之处在于,它是一直处于运行状态的,绑定在这个 Query 中的回调代码会在每次数据改变时被调用。 并且 HKObserverQuery 还支持 background delivery

示例代码:

1
2
3
4
5
6
7
HKQuantityType *bloodSugar = ...
HKObserverQuery *query;
query = [[HKObserverQuery alloc] initWithSampleType:bloodSugar
            predicate:nil
            updateHandler:^(HKObserverQuery *query, HKObserverQueryCompletionHandler handler, NSError *error) {
    NSLog(@"Updated!");
}]

HKAnchoredObjectQuery

作为 HealthKit 的数据提供方,用户每次添加了新数据后,等到一个何时的时机,需要向 HealthKit 的云端数据库中同步数据。开发者可能会想到获取所有的数据,再判断哪些数据需要上传。而 HKAnchoredObjectQuery

HKAnchoredObjectQuery 可以设置限制数,还可以设置一个锚(Anchors)。等一下…锚是什么东西啊喂….

我们来看这两张图:

1.这是 anchor=0 时我们会获取所有的数据

2.当 anchor=3 时我们会获取从第四个数据开始之后的所有数据

这其实就是个索引一样的东西…anchor 应该设置为我们最后一次见到的数据的 anchor 值,如果还没有见到过任何数据,可以将 anchor 设置为0,在获取数据的 callback 中我们会得到新的 anchor。

1
2
3
4
5
6
7
8
9
10
11
12
self.lastAnchor = 0
...
HKQuantityType *bloodSugar = ...
HKAnchoredObjectQuery *query;
query = [[HKAnchoredObjectQuery alloc] initWithType:bloodSugar
                predicate:nil
                anchor:self.lastAnchor
                limit:HKObjectQueryNoLimit
                completionHandler:^(HKAnchoredObjectQuery *query, NSArray *results, NSUInteger newAnchor, NSError *error) {
    self.lastAnchor = newAnchor;
    NSLog(@"Results: %@", results);
}];

执行 Query

执行 queries 的方法在 HKHealthStore 中。

1
2
3
4
@interface HKHealthStore :NSObject
    - (void)executeQuery:(HKQuery *)query;
    - (void)stopQuery:(HKQuery *)query; // 调用这个方法会停止查询数据,并且阻止 query 中的回调被执行
@end

一般只有长时间运行的 queries 需要被停止。

获取统计数据

我们可以获取所有的数据,然后手动计算出我们想要的统计数据。但是我们也可以使用系统提供的 HKStatistics对象获取统计数据。

HKStatistics

  • 可以获取几种统计数据:Sum、Min、Max、Average
  • 获取指定数据源的所有数据
  • HKStatistics 只支持获取 quantity 类型的数据

Classifying Types

不是所有的类型都一样的,比如能量消耗值,我们会关心的是最小值、最大值和平均值,所有能量消耗的总和的意义看起来没有其它几个值重要。

  • Discrete 类型只关心:最小值、最大值和平均值
  • Cumulative 类型只关心:总和

HKQuantityType 总是 discrete 或者 cumulative,它有一个 aggregationStyle的属性,可以通过这个属性来判断一个 HKQuantityType 是什么类型的。

回到HKStatistics,我们可以指定一个 HKStatisticsOptions 告诉 HKStatistics 我们需要什么样的统计数据

HKStatisticsQuery

如何使用HKStatisticsQuery 获取今天走的步数

1
2
3
4
5
6
7
8
9
10
11
HKQuantityType *stepCount = ...
NSPredicate *today = ...
HKStatisticsOptions sumOptions = HKStatisticsOptionCumulativeSum;
HKStatisticsQuery *query;
query = [[HKStatisticsQuery alloc] initWithQuantityType:stepCount
                quantitySamplePredicate:today
                options:sumOptions
                completionHandler:^(HKStatisticsQuery *query, HKStatistic *result, NSError *error) {
    HKQuantity *sum = [result sumQuantity];
    NSLog(@"Steps: %lf", [sum doubleValueForUnit:[HKUnit countUnit]]);
}];

HKStatisticsCollection

如果我们想要获取多个统计数据,我们可以使用HKStatisticsCollection, 我们可以指定一个时间段做为统计数据的间距,比如 24 小时为一个统计单元,HKStatisticsCollection 中的数据就会是每个 24 小时内的统计统计数据的集合。

图3 HKStatisticsCollection 中的数据示意图

图3 HKStatisticsCollection 中的数据示意图

HKStatisticsCollectionQuery

如何使用:

  • 设置一个 Statistics options
  • 设置 Anchor date
  • Interval componets
  • 获取 Collection

代码类似其它 Query 的代码,就不写了。

HealthKit 最佳实战

开始使用 HealthKit 首先需要在项目中开启 HealthKit 功能。方法是项目 target -> Capabilities -> 开启 Health Kit 功能

关于隐私和权限

  • HealthKit 中的数据对用户来说可能是非常敏感的数据
  • 不同类型数据的权限是分开的
  • 读/写权限是分开的

在使用 HealthStore 之前开发者需要先使用下面的方法请求权限。

1
2
3
- (void)requestAuthorizationToShareTypes:(NSSet *)typesToShare
                readTypes:(NSSet *)typesToRead
                completion:(void(^)(BOOL success, NSError *error))completion;

其中,typesToshare 用于指定读权限的类型,typesToRead 指定写权限的类型。一旦程序中请求了权限,系统会弹出一个设置权限的界面,如下图:

图4 请求 HealthKit 多个类型权限的弹出界面

图4 请求 HealthKit 多个类型权限的弹出界面

请求完权限后,在程序中,可以通过 - (HKAuthorizationStatus)authorizationStatusForType:(HKObjectType *)type;查看是否获得了指定类型的权限。

总结

对于开发者来说 HealthKit 给应用带来更多的可能性,开发者可以通过获取 HealthKit 内的信息让自己的 App 内容更佳充实。

引用

这篇文章所有内容来自WWDC Session 203 Introducing Healthkit

iOS UITextView 输入内容实时更新 cell 的高度

这篇文章介绍了在一个动态数据的 table view 中,cell 根据 text view 内容的输入实时改变 cell 和 table view 的高度。自动计算 cell 高度的功能使用 iOS 8 才支持的自适应 cell,如果你还不知道 iOS 8 自适应 cell, …… Continue reading

iOS 8 自适应 Cell

Published on November 13, 2014