iOS8からHealth.appとHealthKitというフレームワークが追加されました。
HealthKitはCoreDataのようなアプリ間で共有できる健康データの読み書きや健康情報に関する単位やformatter、統計計算が行えるフレームワークです。
HealthKitが扱う範囲は幅広いため、この記事ではデータの書き込みと読み込みを例にHealthKitの基本的な使い方について学んで行きたいと思います。
2014年7月25日(金)に第1回 Tech-Gym byプラスアール @SMS 【iOS勉強会、開発者向け】 : ATNDというイベントで、Healthkitについて喋ります。
詳細はTech-GymというiOS勉強会を開催しますの方を見て下さい。
基本的なクラス
HealthKitではかなりの数のクラスや単位の定義等が用意されています。
単位を表すHKUnitや量を表すHKQuantity、身長や血圧など具体的な種類を表すHKObjectTypeを始め、
Health.appに保存したデータを検索するHKQueryなど多種多様です。
この記事ではクラスについては細かくは触れないため、先に以下の記事を読むと理解がし易いかもしれません。
読み書きの権限を取得
HealthKitで扱うデータはセンシティブなデータであるため、
連絡帳の読み出しのようにユーザーに許可をもらってから出ないと読み書きする事が出来ません。
また、HealthKitを扱うアプリでは、事前にKeyChainやMap等と同じようにCapabilityでHealthKitの項目をONにする必要があることに注意して下さい。
(これはXcode5ですが大体同じような感じでXcode6βにHealthKitが増えてます)
これをONにしていないと、認証を貰うコードすら動かなくなり、エラーなのかどうかすらわからなくなります。
まずは、データを書き込みサンプルを例に認証->書き込みの流れを見て行きたいと思います。
書き込みをするサンプル
シンプルに身長を記録するアプリを書いてみます。
サンプルコードは以下にあります(細かいコードは解説してないのでサンプルを一緒に見るといいです)
- azu/WriteHealthKit
- Capabilityは自分で設定し直す必要があるかも
入力するデータとしては以下の2種類だけという最低限のものです。
- 身長(cm)
- 記録した日付
通常のアプリでCoreDataなどに記録する場合は、heightというdoubleの値を保持するattributesを持つEntityを作って、保存するという事をやると思います。
HealthKitも大枠的にやっていることは同じですが、
HealthKitの場合はNSmanagedObjectではなくて、HKObjectという抽象クラスを経由して読み書きします。
HKObjectはHKQuantityType(型)とHKQuantity(量)とHKUnit(単位)から構成されるオブジェクトで、型と量と単位という感じにオブジェクトです。
HealthKitではこの型と単位がデフォルトセットで大量に用意されていて、多くの場合はこの中から組み合わせることが利用できます。(独自の単位なども作れます)
例えば、今回の身長なら
という型があり、この型に適切な単位と身長の値を組み合わせてHKObjectを作成します。
身長に対する単位は
クラスにそれぞれ定義されていて、 cmという単位はそのままではないため、今回はmeterに直して保存します。
@interface HKUnit (Length)
+ (instancetype)meterUnitWithMetricPrefix:(HKMetricPrefix)prefix; // m
+ (instancetype)meterUnit; // m
+ (instancetype)inchUnit; // in
+ (instancetype)footUnit; // ft
+ (instancetype)mileUnit; // mi
@end
この型と単位の組み合わせが間違っている場合は、実行時エラーとなるようです。
詳細はサンプルを見ていただくといいのですが、 このサンプルでは、self.heightDataに入ったNSNumberの身長から、 さきほどの型と単位を組み合わせてHKObjectを返すメソッドを作っています。
HKQuantitySample は HKObjectを継承したサブクラスで、特定のタイミングに計測された量を表すデータを入れます。
HKObjectの構成についてはHealthKit 入門 1 – I’m Sei.がとても分かりやすく書かれているので、こちらを読むことをオススメします。
- (HKQuantitySample *)heightSample {
if (self.heightData == nil) {
return nil;
}
HKQuantityType *heightType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeight];
// double/cm
double meterValue = self.heightData.doubleValue / 100;
HKQuantity *height = [HKQuantity quantityWithUnit:[HKUnit meterUnit] doubleValue:meterValue];
HKQuantitySample *heightSample = [HKQuantitySample quantitySampleWithType:heightType quantity:height startDate:self.recordDate endDate:self.recordDate];
return heightSample;
}
これで、保存する値となるHKObjectを作れたので、 後はこれ保存するだけなのですが、先に述べたようにHealthKitにはユーザーに書き込み許可を貰う必要があります。
書き込み権限の取得
HealthKitでは書き込み(ShareTypes)と読み込み(readTypes)で権限が分けられていて、ユーザーが細かく制御出来るようになっています。
この許可を貰うには、
のインスタンスメソッドである以下のメソッドを利用します。
- (void)requestAuthorizationToShareTypes:(NSSet *)typesToShare
readTypes:(NSSet *)typesToRead
completion:(void (^)(BOOL success, NSError *error))completion;
サンプルでは、書き込みや許可を処理(つまりHKHealthStoreの処理)をラップしたDataStoreManagerというクラスを作って利用しています。
上記のAPIをみて分かるように基本的にHKHealthStoreのメソッドはコールバックを使った非同期処理です。
コールバックは直接扱うとすぐネストが深くなりやすいため、今回はPromiseKitを使ったPromiseラッパをそれぞれのAPIに作っています。
例えば、DataStoreManagerではあるHKQuantityType(型)の権限を貰う処理のPromiseラッパが定義してあります。(今回はひとつのTypeのみでしたが、NSSetで複数のtypeをまとめることが出来ます)
- (PMKPromise *)authorizationToType:(HKQuantityType *) quantityType {
NSSet *dataTypes = [NSSet setWithObject:quantityType];
// authorization for write
return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) {
[self.healthStore requestAuthorizationToShareTypes:dataTypes readTypes:nil completion:^(BOOL success, NSError *error) {
if (!error) {
fulfiller(nil);
} else {
rejecter(error);
}
}];
}];
}
を呼ぶと、自動でモーダルUIでユーザーに書き込み/読み込み権限の許可を貰う画面が出てくるので、それを入力してもらいます。(サンプルを動かしてみるのが分かりやすいです)
読み込み権限が必要ない場合はreadTypesにnilを指定しておけば、権限の許可を貰う画面には出てこなくなります。
requestAuthorizationToShareTypesの非同期処理(ユーザーが選択してる処理)が終わったらコールバックが呼ばれて、エラーがなければ
を呼び正常終了として次の処理に行くという、PMKPromiseのインスタンスを返しています。
ここで気をつけて欲しいのが、たとえerrorがnilであっても権限がもらえたかどうかは別という点です。
このコールバックはただ単に権限の画面が正しく閉じられたかぐらいの意味しかありません。
実際に権限が取得できたかはauthorizationStatusForTypeを使うことで判定出来ます。
authorizationStatusForTypeは同期的に権限の取得状況をチェック出来ますが、他のAPIと同じようにPromiseラッパを書いて非同期処理として扱います。
先ほと同じように、DataStoreManagerに以下のようなPMKPromiseのインスタンスを返すメソッドを追加します。
- (PMKPromise *)authorizationStatusForType:(HKQuantityType *) quantityType {
NSSet *dataTypes = [NSSet setWithObject:quantityType];
// authorization for write
return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) {
HKAuthorizationStatus status = [self.healthStore authorizationStatusForType:quantityType];
switch (status) {
case HKAuthorizationStatusNotDetermined:
rejecter([NSError errorWithDomain:@"HKAuthorizationStatus" code:status userInfo:nil]);
case HKAuthorizationStatusSharingDenied:
rejecter([NSError errorWithDomain:@"HKAuthorizationStatus" code:status userInfo:nil]);
case HKAuthorizationStatusSharingAuthorized:
fulfiller(nil);
}
}];
}
これで、権限を取得と確認するメソッドができました。 結局はどちらも一緒に使うので、ひとつのメソッドにまとめてみましょう。
- (PMKPromise *)availableType:(HKQuantityType *) hkQuantity {
return [self authorizationToType:hkQuantity].then(^{
return [self authorizationStatusForType:hkQuantity];
});
}
このメソッドでは特定のTypeの権限取得 -> チェック をしています。 既に取得済み or その場で取得できたら fulfiller がよばれ、それ以外ならrejecterが呼ばれるという感じですね。
データを書き込む
最初に
で書き込むデータ自体は作成していたので、
実際にHealthKitにデータを保存する処理を書いていきます。
HealthKitでデータを保存すると、Health.appに保存されて複数のアプリ間で使える共有のデータベースとなります。
データを保存する処理はHKHealthStoreの
を利用します。
見て分かるようにこれもコールバックを受け取る非同期処理です。
これも他のAPIと同じようにPromiseラッパを作成します。
- (PMKPromise *)writeSample:(HKQuantitySample *) sample {
return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) {
[self.healthStore saveObject:sample withCompletion:^(BOOL success, NSError *error) {
if (!error) {
fulfiller(nil);
} else {
rejecter(error);
}
}];
}];
}
書き込むのはHKObjectのサブクラスであるHKQuantitySampleです。
これでやっと権限の取得、権限のチェック、データの保存のAPIが揃ったので、後はこれを並べるだけです。
“save” ボタンのハンドラに以下のような処理を書きます。
- (IBAction)handleSaveButton:(id) sender {
PMKPromise *availableTypePromise = [self.dataStoreManager availableType:self.model.managedType];
availableTypePromise.then(^{
return [self.dataStoreManager writeSample:self.model.heightSample];
}).then(^{
UIAlertView *savedAlert = [[UIAlertView alloc] initWithTitle:nil message:@"Saved!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[savedAlert show];
}).catch(^(NSError *error) {
UIAlertView *savedAlert = [[UIAlertView alloc] initWithTitle:@"Error" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[savedAlert show];
NSLog(@"error = %@", error);
});
}
基本的なPromiseのコントロールフローを利用する事でキレイな処理を書くことが出来ます。
具体的にこの処理では以下のような流れが書かれています。
- availableType
- 権限の取得
- 権限のチェック
- 権限が取得できていたら(then)、writeSampleでデータを書き込む
- データの書き込みに成功したら(then)、”Saved”と表示する
- 権限の取得に失敗 or データの書き込みに失敗(catch)なら”Error”を表示
これはPromiseラッパを書いたことで、このようなコントロールフローが実現出来ます。
(NSOperationでも頑張ればできる)
- PromiseKit
- JavaScript Promiseの本
- JavaScriptですが基本的な流れは同じです
これでデータをHealth.appにデータを書き込む事が出来ました。
細かいコードはazu/WriteHealthKitを見てみて下さい。
恐らくCapabilityが外れているので、自分のApple IDで設定し直す必要があるかもしれません。
次は、Health.appに保存されているデータを読み込むアプリを作っていきたいと思います。
データを読み出すアプリ
Health.app からも直接データを書き込む事ができますが、先ほどの書き込むアプリを使って幾つか適当に身長データを記録しておきます。
今度はHealth.appに保存されている身長データを取り出して一覧表示するだけのシンプルなアプリを作ってみたいと思います。
ソースコードは以下に置いてあります。
- azu/ReadHealthKit
- Capabilityの設定を忘れずに
基本的には先程のazu/WriteHealthKitと殆ど同じで、
データを取得する
というメソッドをDataStoreManagerに追加した感じとなっています。
データの読み出す
HealthKitでデータを読み出すには
という検索するためのクエリーオブジェクトが利用できます。
検索方法も単純な検索から、監視、特定位置からの検索等豊富に用意されていますが、今回は単純に全てのデータを取り出すHKSampleQueryを作成します。
実際にはPromiseラッパとしてのメソッドを追加したので、以下のようになっています。
- (PMKPromise *)fetchAllSampleForType:(HKQuantityType *) quantityType {
return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) {
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:HKSampleSortIdentifierEndDate ascending:NO];
HKSampleQuery *sampleQuery = [[HKSampleQuery alloc] initWithSampleType:quantityType predicate:nil limit:0 sortDescriptors:@[sortDescriptor] resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
if (!error) {
fulfiller(results);
} else {
rejecter(error);
}
}];
[self.healthStore executeQuery:sampleQuery];
}];
}
HKSampleQueryでは、検索するデータの種類やソート条件、predicate等が指定出来ます。
結果はコールバックなので、取得出来た場合はfulfiller、出来なかった場合はrejecterを呼ぶとなっています。
作ったクエリは[self.healthStore executeQuery:sampleQuery];という感じでHKHealthStoreで呼ぶ出すことで取得が始まります。
後は、この取得したデータをTableViewに表示するだけです。
CoreDataと違って取得も非同期であるため、普通にコールバックでやるとMain Threadで明示的に呼ぶ出すコードが必要になるなど意外と大変です。
PromiseKitはthenの中をmain threadで実行するなど融通を効かせてくれます。
サンプルのazu/ReadHealthKitでは、表示をするHealthDataTableViewControllerとデータを管理するHealthDataTableViewModelに分けて実装してあります(ViewModelという名前になってるのは特に意味ないです)
取得したデータを表示する
次に、HealthDataTableViewModelの方にデータを取得するメソッドを追加してあげます。
- (PMKPromise *)reloadData {
PMKPromise *availablePromise = [self.dataStoreManager availableType:self.managedType];
return availablePromise.then(^{
return [self.dataStoreManager fetchAllSampleForType:self.managedType];
}).then(^(NSArray *result) {
self.heightDataList = result;
return result;
});
}
PMKPromiseを返しているのは、データを取得完了してからじゃないとTableViewも更新出来ないので、
データを取得できたら〜という処理をControllerに書くためです。
取得したデータは HealthDataTableViewModel の方に self.heightDataList として保持しておきます。
そして、Controller側でデータ取得 -> TableViewの更新 という処理を書いてあげれば殆ど完成です。
- (void)updateTableView {
[self.model reloadData].then(^{
[self.tableView reloadData];
}).catch(^(NSError *error) {
UIAlertView *savedAlert = [[UIAlertView alloc] initWithTitle:@"Error" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[savedAlert show];
NSLog(@"error = %@", error);
});
}
取得したデータの配列にはHKQuantitySampleのインスタンスが入ってます。
HKQuantitySampleは書き込み使ったものと同じなので、書き込んだ日付やデータの型や量等が入ってるので後はそれを表示するだけですね。
これでデータを読み込んで表示するazu/ReadHealthKitの紹介は終わりですが、
HealthKit 入門 2 – I’m Sei.で紹介されているようにHealthKitの面白いところはこの取得する機能が充実してる点です。
単純にデータを取得するだけではなく、HKStatisticsQueryを使って合計を計算して取得したり、SeparateBySourceで特定の曜日のデータだけを取り出して計算するなどが簡単にできるようになっています。
また、今回のサンプルで示したように、書き込み用のアプリと読み込み用のアプリが完全に別アプリとしても動かせるという点が、色々なタイプのアプリを作れる余地を残している気がします。(例えばビューアー特化のアプリとかが作りやすい)
この記事ではiOS8 β3時点でのHealthKitの読み書きについて紹介しました。
データの抽象度が結構あるので扱いにくい部分もありますが、その分柔軟に色々な事に使えそうな気がしています。
この記事で書いたようなこと等も含めてHealthKitなどについて、2014年7月25日(金)に開催する勉強会で発表するつもりです。