18
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

SENSYAdvent Calendar 2017

Day 13

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

Last updated at Posted at 2017-12-12

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

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

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

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

私はiPhoneとAppleWatchを身につけてランニングしていますが、
ランニングの情報を記録するアプリは山ほどあり、
それらのアプリで記録した結果は大抵の場合iOS標準アプリの アクティビティ アプリ内にある
ワークアウト に記録されているかと思います。
こんな感じ。
IMG_6631.PNG

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

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

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

前提

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

まずはProjectを作成し、CapabilitiesとInfo.plistを編集

Workout関連のデータを扱う際は、 HealthKit を使用します。
そのための設定として

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

Capabilitiesは見れば分かると思うので、割愛。
スクリーンショット 2017-12-12 16.32.20.png

  • Info.plist
    以下のキーを追加します。

    key : Privacy - Health Share Usage Description

    value : please arrow
スクリーンショット 2017-12-12 16.30.26.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)
    }
}

上記では、

let healthKitStore: HKHealthStore = HKHealthStore()

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

self.healthKitStore.requestAuthorization(toShare: nil, read: readDataTypes)

このコードで、引数のreadに与えたTypeに対してiOS側で許可Dialogを出してくれます。
また、許可状態がSuccessだった場合の処理も中に記載しています。

func readRunningWorkOuts

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

動かしてみる

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

Appを起動すると、以下のようにまずはヘルスケアデータへの許可を聞いてきてくれます。
IMG_6632.PNG

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

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

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

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))
        // ここまで
    });
})

さて、これで改めて起動してみます。

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ですが、

func stringFromTimeInterval(interval: TimeInterval) -> String

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

func calcAverageSpeedForMeter(interval: TimeInterval, distance: Double) -> String 

こちらではDuration(所要時間)とDistance(距離)から平均速度を求めています。

workout.totalDistance!.doubleValue(for: HKUnit.meter())

こうするとDistanceを指定したHKUnitの単位で取得できるんですね。

いい感じのデータができた

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

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

謝辞

当記事はこちらのページを参考にさせていただきました。 :bow:
StackPOverFlow

18
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?