目次
はじめに
Apple Watchが発売されて早数年。
iPhoneユーザーでない私にはあまり縁のないモノでしたが、先日業務で、Apple Watchで様々なデータを収集・管理するアプリを開発する機会があったので紹介しようと思います。
開発時に使用した環境は以下であるため、情報が古い可能性があります。
項目 | バージョン |
iOS | 12.3 |
WatchOS | 5.2 |
XCode(統合開発環境) | 10.3 |
Swift | 5 |
プロジェクトの準備
早速、進めていきましょう。
プロジェクト作成
XCodeを立ち上げ、新規プロジェクトを作成します。その際に、OSは「watchOS」、アプリひな型は「iOS App with WatchKit App」を選択してください。
後は、必要な情報を入力してプロジェクトの作成を完了させてください。プロジェクトのオプションはデフォルトのままで問題ありません。
プロジェクトを作成すると、プロジェクトナビゲータに様々なファイルが表示されます。以降は、赤枠で囲った部分に修正を行っていきます。
ひな型が「iOS App with WatchKit App」となっていたように、Apple WatchアプリはペアリングされたiPhone側のアプリと連動して動作します。(1)はこのiPhone側のアプリのUI・動作を記述します。
(2)はApple Watch側のアプリのUI部分を記述します。
(3)がApple Watch側のアプリの動作内容を記述する部分となります。
HealthKitの使用準備
今回のアプリでは、Apple Watchが常に収集・蓄積を続けている心拍数を表示しようと思います。
心拍数はHealthKitというApple Watch・iPhone間で共有するフィットネスデータリポジトリに蓄積されています。HealthKitに蓄積されたデータは機密情報として扱われるため、ライブラリの追加以外に、権限設定を行う必要があります。また、後程出てきますが、アプリを使用するユーザーが明示的に権限をオンにする操作も必要になります。
まず、プロジェクトにHealthKitを組み込みます。
「(a)プロジェクトナビゲータの青いアイコン」 > 「(b)エディタ領域に表示されるTARGETS配下のHeartRate WatchKit Extension」 > 「(c)Capabilities」の順にクリックします。
表示されたリストから「(d)HealthKitをON」に変更すれば、組み込み完了です。
※以下画面キャプチャの赤枠部分を参考にしてください。
次に、権限設定です。「(e)Info」をクリックします。
WatchOS Target Propertiesのリストの任意の項目にマウスオーバーすると、(f)のように右側に+/-のボタンが表示されるので、+ボタンをクリックします。
選択行の下にプルダウンリストが表示されるので、リストから「Privacy – Health Share Usage Description」を選択します。
最後に「(g)Value」に適当な文章を入力します。今回は「For Health Care」としています。最初、Valueに日本語を設定したところ、アプリ起動時に例外が発生してしまいました。
(a)~(g)の手順を「TARGETS配下のHeartRate」に対しても行います。
HealthKitデータの読み込みはApple Watch側のみで実施する予定なのですが、先ほど記載した「アプリを使用するユーザーが明示的に権限をオンにする操作」がiPhone側で実行されるため、iPhone側のアプリにも権限等を設定する必要があるようです。
UIのデザイン
Apple WatchのUIデザインは、HeartRate WatchKit AppのInterface.storyboardで行います。今回は1画面に必要な情報を並べただけの簡単な画面にします。必要なパーツ(赤枠部分)を配置するだけです。
各項目については以下のとおりです。
- スイッチ:Workout状態設定用のスイッチ
- 最新:最新の心拍数を表示するラベル
- 平均:最後に実施したWorkout期間中の平均心拍数を表示するラベル
- 最大:最後に実施したWorkout期間中の最大心拍数を表示するラベル
- 最小:最後に実施したWorkout期間中の最小心拍数を表示するラベル
HealthKitが運動・トレーニングを管理するための情報がWorkoutです。通常時の心拍数蓄積は数分毎なのですが、Workout中は数秒毎に変更されるため、Workoutの開始・終了の制御ができるようにしました。
また、ただ最新の心拍数の値が短時間に更新されるだけではあまりおもしろくないので、Workout実行期間中の平均・最大・最小を表示するようにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var dateWorkoutSessionStart: Date? var dateWorkoutSessionEnd: Date? // Workout状態設定スイッチ操作時の処理ハンドラ @IBAction func switchActionWorkoutStatus(_ value: Bool) { if (value) { // WorkoutSession開始 self.startWorkoutSession() self.dateWorkoutSessionStart = Date() self.dateWorkoutSessionEnd = nil self.heartRateWOAverage = -1.0 self.heartRateWOMax = -1.0 self.heartRateWOMin = -1.0 } else { // WorkoutSession終了 self.stopWorkoutSession() self.dateWorkoutSessionEnd = Date() } } |
Workout状態設定用のスイッチを操作した際のハンドラの実装は上記のとおりです。startWorkoutSession()、stopWorkoutSession()に関しては後程説明します。
dateWorkoutSessionStart、dateWorkoutSessionEndにはWorkoutの開始日時、終了日時を記憶します。Workoutの終了が完了した時点で、これらの日時を使用して、Workout期間中の心拍数を取得・表示します。
1 2 3 4 5 6 7 8 9 10 11 |
@IBOutlet weak var labelHeartRateLatest: WKInterfaceLabel! var heartRateLatest: Double = 0.0 { didSet { if self.heartRateLatest < 0.0 { labelHeartRateLatest.setText("最新 : ----") } else { labelHeartRateLatest.setText("最新 : \(self.heartRateLatest)") } } } |
ラベル関連の実装は上記のとおりです。最新心拍数を表示するラベルを例としていますが、ほかのラベルも基本は同じです。
心拍数は負の値にならないので、負の値が設定された場合に初期表示に戻すようにしています。
5行目から10行目の内容は、インスタンス変数heartRateLatestが更新された際に実行される処理です。UIへの表示はこの中に実装し、ほかの処理からはインスタンス変数の更新のみを行うようにしています。
HealthKit認証処理の実装
HealthKitの使用準備の節でも記載した「アプリを使用するユーザーが明示的に権限をオンにする操作」をHKHealthStoreクラスの以下メソッドを使用して実装します。
1 2 3 4 |
func requestAuthorization( toShare typesToShare: Set<HKSampleType>?, read typesToRead: Set<HKObjectType>?, completion: @escaping (Bool, Error?) -> Void) |
第一引数typesToShareはアプリから書き込みたいHealthKit項目のリストを指定します。
第二引数typesToReadはアプリで読み出したいHealthKit項目のリストを指定します。
第三引数completionは認証処理完了時に実行されるハンドラで、Bool型の第一引数には処理結果、Error型の第二引数にはエラー発生時の情報が格納されます。
また、今後HealthKitを使用するために必要なHKHealthStoreのインスタンスの準備も行います。HKHelathStoreインスタンスはアプリで一つあればよいので、インスタンス変数として定義します。
Apple Watch側
Apple Watch側アプリへの実装は、HeartRate WatchKit ExtensionのInterfaceController.swiftに行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// HealthKit 関連データ let hkStore = HKHealthStore() var hkIsAuthorized = false let hkTypeHeartRate = HKObjectType.quantityType(forIdentifier: .heartRate)! // 定周期処理 var timerReadHK: Timer? let timerIntervalReadHK = 5.0 override func willActivate() { // This method is called when watch view controller is about to be visible to user super.willActivate() // HealthKit関連初期化処理 // ※HealthKit を使用できる場合のみ表示する if (HKHealthStore.isHealthDataAvailable()) { // HealthKit の使用許可ダイアログの表示 let readTypes: Set<HKObjectType> = [self.hkTypeHeartRate] self.hkStore.requestAuthorization(toShare: nil, read: readTypes, completion: {(success, error) -> Void in if success { NSLog("HealthKit Request Authorization succeeded") self.hkIsAuthorized = true } else if let error = error { NSLog("HealthKit Request Authorization error \(error)") } else { NSLog("HealthKit Request Authorization error") } }) // HealthKit定周期読み出し開始 self.startReadHK() } } // HealthKit データ読み出しの開始 func startReadHK() { self.timerReadHK = Timer.scheduledTimer(timeInterval: self.timerIntervalReadHK, target: self, selector: #selector(self.timerHandlerReadHK), userInfo: nil, repeats: true) } |
今回は書き込みを行わないので第一引数は nil、第二引数には心拍数のみを指定します。
認証処理完了ハンドラで処理成功した際に設定しているhkIsAuthorizedは、認証未完了の状態で心拍数読み込みを実行しないようにするためのフラグです。
iPhone側
iPhone側の実装についても、最後の startReadHK()の呼び出しを行わないことを除けば同じ内容です。
iPhone側はHeartRateのViewController.swiftのviewDidLoad()に実装してください。
心拍数読み出し処理
次は、今回のメインである、HealthKitからの心拍数読み出し処理の実装です。
HealthKitからのデータの読み出しは、以下の手順で行います。
-
-
- クエリの構築(ソート、読み出し件数、条件等の指定が可能)
- クエリの実行
- クエリ実行完了時ハンドラの実行(構築したクエリに対応するデータが渡される)
-
心拍数取得のクエリは、 HKSampleQueryクラスにパラメータを指定して生成することで構築します。HKSampleQueryクラスのコンストラクタ定義は以下のとおりです。
1 2 3 4 5 6 |
init( sampleType: HKSampleType, predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) |
第一引数sampleTypeには、読み出す対象のHelathKit項目を指定します。HealthKit認証処理時に指定したものと同じ内容を指定することになります。
第二引数predicateには、データ読み出し時の条件を指定します。期間や値の範囲などを指定することができます。HKQueryクラスのpredicateFor****()メソッドで生成した内容を指定しますが、指定しないこともできます。指定しない場合はすべてのデータが読み出せます。
第三引数limitには、読み出すデータの最大個数を指定します。
第四引数sortDescriptorsには、読み出し結果の順序を指定します。日付・値などを昇順・降順に並べることができます。
第五引数resultsHandlerには、クエリ実行完了時に実行されるハンドラを指定します。第一引数には実行したクエリそのものが、第二引数には読み出し成功時のデータリストが、第三引数にはエラー発生時の情報が格納されます。
読み出したデータから、実際の心拍数を取得するには、以下の手順を行います。
-
-
- HKQuantitySumple型にキャストする
- HKQuantity型のメンバ変数quantityのdoubleValue()メソッドを呼び出す。
-
上記2の手順を実施する際には、読み出す項目に対して適切な単位を指定する必要があります。HealthKitには、「m」や「g」、「秒」といった基本的な度量衡は準備されているのですが、心拍数で使用する「拍/分」はどうやら定義されていないようです。HKUnit()クラスに生成したい単位系の文字列を指定して単位の情報を生成する必要があります。
ちなみに「bpm」では単位情報の生成に失敗します。Beats Per Minuteくらい解釈してくれてもよいのに……。
なお、上記の読み出し処理は iPhone側でも全く同じ実装で実現できます。
最新心拍数読み出し
まずは、定周期で行う最新情報の取得処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
let hkUnitHeartRate = HKUnit(from: "count/min") // 通常時の心拍数情報更新処理 func updateHeartRateNormal() { if self.hkIsAuthorized { let sortDescriptors = [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)] let query = HKSampleQuery(sampleType: self.hkTypeHeartRate, predicate: nil, limit: 1, sortDescriptors: sortDescriptors, resultsHandler: { (query, samples, error) -> Void in if let error = error { NSLog("[updateHeartRateNormal] resultHandler error: \(error.localizedDescription)") } else if let samples = samples { let sample = samples[0] as! HKQuantitySample NSLog("[updateHeartRateNormal] resultHandler sample: \(sample.debugDescription)") let value = sample.quantity.doubleValue(for: self.hkUnitHeartRate) DispatchQueue.main.async { self.heartRateLatest = value } } } ) self.hkStore.execute(query) } } |
最新の1件が読み出せればよいので、sortDescriptorsに日付降順、limitに1件を指定しています。
6行目から18行目がクエリの構築、10行目がクエリの実行になります。8行目から17行目はクエリ実行完了時に実行されるハンドラの内容です。ハンドラ内15行目では、読み出したデータで最新の心拍数を表示するラベルを更新しています。
Workout期間中の心拍数読み出し
続いて、Workout期間中の心拍数取得処理です。
Workoutの開始日時、終了日時は、UIに配置したスイッチのハンドラでインスタンス変数に保持するようにしています。このWorkout開始日時・終了日時を指定して以下のメソッドを実行することで、Workout中の平均心拍数・最大心拍数・最小心拍数を取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// WorkoutSession時の心拍数情報更新処理 func updateHeartRateWO(start: Date, end: Date) { if self.hkIsAuthorized { let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: []) let query = HKSampleQuery(sampleType: self.hkTypeHeartRate, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil, resultsHandler: {(query, samples, error) -> Void in if let error = error { NSLog("[updateHeartRateWO] resultHandler error: \(error.localizedDescription)") } else if let samples = samples { // 値取り出し let values = samples.map{ ($0 as! HKQuantitySample).quantity.doubleValue(for: self.hkUnitHeartRate) } NSLog("[updateHeartRateWO] resultHandler values: \(values)") // 平均値取得 let total = values.reduce(0, +) let average = total / Double(values.count) self.heartRateWOAverage = average NSLog("[updateHeartRateWO] resultHandler Average value:\(average)") // 最大値取得 if let max = values.max() { self.heartRateWOMax = max NSLog("[updateHeartRateWO] resultHandler Max value:\(max)") } // 最小値取得 if let min = values.min() { self.heartRateWOMin = min NSLog("[updateHeartRateWO] resultHandler Min value:\(min)") } } }) self.hkStore.execute(query) } } |
こちらでは、特定期間の心拍数データが欲しいので、predicateを利用します。4行目で引数で指定されたWorkout開始日時から終了日時のデータを取得するためのpredicateを生成しています。最新の情報と違い、指定期間中に何件のデータが蓄積されたかわからないため、HKSampleQuery()のlimitにはHKObjectQueryNoLimitを指定しています。
また、期間中の平均・最大・最小を取得するのが目的なので、データの並び順は考慮しないため、sortDescriptorsは指定していません。
後は、クエリ実行完了ハンドラで、読み出したデータ全件の値を取得し、平均・最大・最小を計算、画面に表示しているだけです。
Workoutの開始・終了処理
Workoutに関する処理についても、HealthKitの機能を使用するので、実装内容を紹介します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// Workout Session 用の拡張定義 extension InterfaceController: HKWorkoutSessionDelegate { // WorkoutSession開始処理 func startWorkoutSession() { let config = HKWorkoutConfiguration() config.activityType = .other do { let session = try HKWorkoutSession(healthStore: self.hkStore, configuration: config) session.delegate = self self.hkWorkoutSession = session session.startActivity(with: nil) } catch let e as NSError { fatalError("*** Unable to create the workout session: \(e.localizedDescription) ***") } } // WorkoutSession終了処理 func stopWorkoutSession() { guard let workoutSession = self.hkWorkoutSession else { return } workoutSession.stopActivity(with: nil) } // 状態変化検出時処理 func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) { NSLog("workoutSession delegate didChangeTo") switch toState { case .running: NSLog("Session status to running") case .stopped: NSLog("Session status to stopped") DispatchQueue.main.async { [weak self] in guard let `self` = self else { return } self.hkWorkoutSession = nil guard let startDate = self.dateWorkoutSessionStart else { return } guard let endDate = self.dateWorkoutSessionEnd else { return } self.updateHeartRateWO(start: startDate, end: endDate) } default: NSLog("Other status \(toState.rawValue)") } } // エラー発生時処理 func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) { NSLog("workoutSession delegate didFailWithError \(error.localizedDescription)") } } |
4行目から17行目が開始処理、19行目から23行目が終了処理です。
開始処理では、今後Workoutを管理するためにHKWorkoutSessionのインスタンスを生成しています。HKWorkoutSession生成時には運動の種類(ウォーキングとかアメリカンフットボールとか)を指定するのですが、今回は運動の種類はあまり重要でないので、「その他」を指定しています。
Workoutの開始・終了は生成したHKWorkoutSessionのメソッドを開始・終了呼び出すだけです。
25行目から43行目は、Workoutの状態が変化した際に呼び出されるハンドラです。
Workoutが終了したことを検知すると、開始日時・終了日時を指定して心拍数取得処理を実行しています。
Workoutの開始日時・終了日時はスイッチのハンドラで保存しているので、こちらでは何もしていません。
動作確認
では、動かしてみましょう。
HealthKit認証処理
Apple Watch側でアプリが起動すると、以下のようにHealthKitへのアクセスを要求します。同時に、iPhone側では、以下のダイアログが表示されます。
iPhone側の「”HeartRate”を開く」をクリックすると、HealthKitの認証画面が表示されます。HealthKit認証処理で実装した通り、「心拍数」の「読み出し」について、許可を要求してきています。
「すべてのカテゴリをオン」をクリックするか、「心拍数」のスイッチをオンすると、右上の「許可」が有効になります。
有効になった「許可」をクリックすると、HealthKit認証処理は完了です。次回の起動からは、すでに認証済みなので、上記の画面は表示されないようになります。
心拍数更新
認証が完了すると、最新の心拍数の表示更新が開始されます。
まだWorkoutが開始されていないので、6分かかってようやく最新の心拍数が更新されました。
次にWorkoutを開始した際の動作です。
スイッチをオンに変更しても、最新の心拍数の更新以外の処理は行われません。写真ではわかりませんが、3分の間に何回も最新の心拍数の表示が更新されていました。
Workout中の平均・最大・最小
スイッチをオフに戻してWorkoutを終了すると、平均・最大・最小の内容が、Workoutを実行していた間のデータで更新されます。
まとめ
今回、心拍計を例にApple Watchアプリの開発方法について紹介しました。心拍計そのものはApple Watchにプリインストールされているアプリにもっと高機能のものがあるので、今回のアプリを使用することはないと思います。ですが、そのアプリがどうやって心拍計の機能を実現しているのか、わかってもらえると思います。
HealthKitだけではなく、おおよそのモバイルデバイスに搭載されている加速度センサー等の各種センサーもApple Watchには搭載されています。それらのデータを組み合わせて、健康管理用のアプリ開発に挑戦してみるのも面白いかもしれません。
tdiでは、最新ICTの価値実証「tdi Competency Lab」サービスを提供しております。ご興味がございましたら、ぜひ当社までお問い合わせください。
執筆者プロフィール
- 組み込み、Web、WindowsアプリにiOSアプリ。入社以来、様々なプラットフォームで開発業務に従事中。
この執筆者の最新記事
- Pick UP!2020.02.18Apple Watchで心拍計を表示するアプリを開発してみた