iOSのワークアウトデータを見せびらかしたい [その1]

こんにちは。
SENSY株式会社のnkhrkです。

業務では主にPythonやnode.jsなどでバックエンドの開発をしています。
前職ではiOS/Android/Rubyで占いアプリを作っていました。

私の趣味はコーヒーを作ることとランニングです。

ランニングやマラソンが趣味の方は分かって頂けると思うのですが、
走った時の結果って、距離や時間、速度などを誰かに見せびらかしたくなります。(よね??)

私はiPhoneとAppleWatchを身につけてランニングしていますが、
ランニングの情報を記録するアプリは山ほどあり、
それらのアプリで記録した結果は大抵の場合iOS標準アプリの ***アクティビティ*** アプリ内にある
***ワークアウト*** に記録されているかと思います。
こんな感じ。
<img width="250" alt="IMG_6631.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/167177/4a9989de-a38d-d6f3-74fa-98ed78cd7f25.png">


ただ、iOSのワークアウトデータはデフォルトだとSNS共有機能はあるものの、
走った距離くらいしかくらいしか共有できません。

ランニングジャンキーとして、これはいただけない。
やはり時間や速度、消費カロリーなども見せびらかしたいところです。

そこで今回はAdventカレンダーということで、iOSのワークアウトデータをSlackにPOSTするアプリを作ってみました。
※この記事は[その1]です。[その1]ではワークアウトデータを抽出する部分までを行います。


# 前提
- iPhoneの***アクティビティ*** アプリ内にワークアウトデータが1件以上あること。
    - もしデータが無い場合は、適当なランニングアプリなどを使い、外に出て5分位散歩してください。

# まずはProjectを作成し、CapabilitiesとInfo.plistを編集
Workout関連のデータを扱う際は、 `HealthKit` を使用します。
そのための設定として

- CapabilitiesでHealthKitをON
- Info.plistに追加
が必要となります。

Capabilitiesは見れば分かると思うので、割愛。
<img width="1109" alt="スクリーンショット 2017-12-12 16.32.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/167177/2708a633-30d8-7b5f-0973-ede9b82043df.png">

- Info.plist
以下のキーを追加します。<BR>
`key : Privacy - Health Share Usage Description`<BR>
`value : please arrow`
<img width="822" alt="スクリーンショット 2017-12-12 16.30.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/167177/42c86d8d-4f40-a23a-68a9-8108eda8b591.png">

# コードでワークアウトデータを引っこ抜く
さて、準備は終わったのでワークアウトデータを抽出してみたいと思います。

```ViewController.swift
import UIKit
import HealthKit

class ViewController: UIViewController {

    let healthKitStore: HKHealthStore = HKHealthStore()
    var workouts: [HKWorkout] = []
    
    let readDataTypes: Set<HKObjectType> =
        [
            HKWorkoutType.workoutType(),
            HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.distanceWalkingRunning)!
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.healthKitStore.requestAuthorization(toShare: nil, read: readDataTypes) {
            (success, error) -> Void in
            if success == false {
                print("can't get permittion")
            } else {
                self.readRunningWorkOuts({ (results, error) -> Void in
                    if( error != nil ) {
                        print("Error reading workouts: \(String(describing: error?.localizedDescription))")
                        return;
                    }
                    
                    self.workouts = results as! [HKWorkout]
                    DispatchQueue.main.async(execute: { () -> Void in
                        for workout in self.workouts {
                            print(workout.startDate)
                            print(workout.endDate)
                            print(String(format: "Distance   : %@", workout.totalDistance ?? "no data"))
                            print(String(format: "EnergyBurn : %@", workout.totalEnergyBurned ?? "no data"))
                        }
                    });
                })
            }
        }
    }
    
    func readRunningWorkOuts(_ completion: (([AnyObject]?, NSError?) -> Void)!) {
        let predicate =  HKQuery.predicateForWorkouts(with: HKWorkoutActivityType.running)
        let sortDescriptor = NSSortDescriptor(key:HKSampleSortIdentifierStartDate, ascending: false)
        let sampleQuery = HKSampleQuery(sampleType: HKWorkoutType.workoutType(),
                                        predicate: predicate,
                                        limit: 0,
                                        sortDescriptors: [sortDescriptor]) {
                                            (sampleQuery, results, error ) -> Void in
                                            if error != nil {
                                                print("Query Error")
                                            }
                                            completion!(results,error as NSError?)
        }
        self.healthKitStore.execute(sampleQuery)
    }
}
```

上記では、

```swift
let healthKitStore: HKHealthStore = HKHealthStore()
```
で定義したHKHealthStoreに対して、HKQueryというQueryを発行して抽出した結果を表示しています。
HealthKitではHKHealthStoreというObjectに対して結果を要求するんですね。
HKQueryではHKWorkoutActivityTypeという、ワークアウトのTypeを絞り込んだりできます。


```swift
self.healthKitStore.requestAuthorization(toShare: nil, read: readDataTypes)
```
このコードで、引数のreadに与えたTypeに対してiOS側で許可Dialogを出してくれます。
また、許可状態がSuccessだった場合の処理も中に記載しています。

```swift
func readRunningWorkOuts
```
このfunctionでは先程のQueryの組み立てと実際の問い合わせを行っています。

# 動かしてみる
では実際に動かしてみましょう。

Appを起動すると、以下のようにまずはヘルスケアデータへの許可を聞いてきてくれます。
<img width="250" alt="IMG_6632.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/167177/c756c066-b9d9-50ab-478f-0804fab04367.png">

ここはポチッと "すべてのカテゴリをオン" を押したいところですが、
一旦、画面左上の "許可しない" を選ぶとXcode上のデバッグログに
`"can't get permittion"`が表示されるかと思います。

気を取り直して改めて起動して、"すべてのカテゴリをオン" を押してから右上の"許可"を選びます。

すると、真っ白の画面ではありますがXcodeのデバッグログにはしっかりワークアウトデータが表示されるとかと思います。

```console
2017-12-09 21:16:53 +0000
2017-12-09 23:13:25 +0000
Distance   : 22.2506 km
EnergyBurn : 1047.58 kcal
2017-12-08 21:27:03 +0000
2017-12-08 22:06:17 +0000
Distance   : 5.06278 km
EnergyBurn : 230.979 kcal
2017-12-01 20:32:19 +0000
2017-12-01 21:31:18 +0000
Distance   : 6.77049 km
EnergyBurn : 306.45 kcal
:
:
```

はい、出ましたね。
時刻はデフォルトだとUTCで保存されているので、適宜直します。

# データが寂しい
- ランニングジャンキーとしては距離だけじゃなく、トータルタイムと平均ペースも出したいところ
    - 出しましょう

まずは 以下のfunctionを追加します。

```ViewController.swift
    func calcAverageSpeedForMeter(interval: TimeInterval, distance: Double) -> String {
        let ti = NSInteger(interval)
        let total_seconds = Int(Double(ti) / distance)
        let seconds = total_seconds % 60
        let minutes = (total_seconds / 60) % 60
        
        return String(format: "%d.%0.2d",minutes,seconds)
    }
    
    func stringFromTimeInterval(interval: TimeInterval) -> String {
        
        let ti = NSInteger(interval)
        let seconds = ti % 60
        let minutes = (ti / 60) % 60
        let hours = (ti / 3600)
        
        return String(format: "%dh %0.2dm %0.2ds",hours,minutes,seconds)
    }
```
次に、`viewDidLoad()`内に以下の出力も追加します。

```ViewController.swift
// ~中略~
DispatchQueue.main.async(execute: { () -> Void in
    for workout in self.workouts {
        print(workout.startDate)
        print(workout.endDate)
        print(String(format: "Distance   : %@", workout.totalDistance ?? "no data"))
        print(String(format: "EnergyBurn : %@", workout.totalEnergyBurned ?? "no data"))
        // 以下追加
        print(String(format: "Duration   : %@", self.stringFromTimeInterval(interval: workout.duration)))
        let km_double = workout.totalDistance!.doubleValue(for: HKUnit.meter()) / 1000
        let averagespeed = self.calcAverageSpeedForMeter(interval: workout.duration, distance: km_double)
        print(String(format: "%@ / km", averagespeed))
        // ここまで
    });
})
```
さて、これで改めて起動してみます。

```console
2017-12-09 21:16:53 +0000
2017-12-09 23:13:25 +0000
Distance   : 22.2506 km
EnergyBurn : 1047.58 kcal
Duration   : 1h 55m 56s
5.12 / km
2017-12-08 21:27:03 +0000
2017-12-08 22:06:17 +0000
Distance   : 5.06278 km
EnergyBurn : 230.979 kcal
Duration   : 0h 37m 27s
7.23 / km
2017-12-01 20:32:19 +0000
2017-12-01 21:31:18 +0000
Distance   : 6.77049 km
EnergyBurn : 306.45 kcal
Duration   : 0h 49m 31s
7.18 / km
:
:
```

だいぶいい感じになりましたね。

追加した2つのfunctionですが、

```swift
func stringFromTimeInterval(interval: TimeInterval) -> String
```
こちらのメソッドではHKWorkoutから取得したduration(所要時間)をStringへフォーマットしています。
HKWorkoutのdurationはTimeInterval型で保存されてるんですね。


```swift
func calcAverageSpeedForMeter(interval: TimeInterval, distance: Double) -> String 
```
こちらではDuration(所要時間)とDistance(距離)から平均速度を求めています。

```swift
workout.totalDistance!.doubleValue(for: HKUnit.meter())
```
こうするとDistanceを指定したHKUnitの単位で取得できるんですね。


# いい感じのデータができた
ここまででiOS内のワークアウトデータをいい感じに取得できました。
[その1]ではここまでとし、次回[その2]にてSlackへのPOSTまでを行いたいと思います。

では、良いランニングライフを！！:runner: :runner: :runner: 

# 謝辞
当記事はこちらのページを参考にさせていただきました。 :bow:
[StackPOverFlow](https://stackoverflow.com/questions/42617229/function-readrunningworkouts-with-completion-in-class-hkmanager-hktutorial-is)