dely Tech Blog

クラシル・TRILLを運営するdely株式会社の開発ブログです

potatotips というテック系イベントでHealthKitの権限に苦労した話を発表しました

こんにちは、クラシルリワードのiOSエンジニア uetyo です!

先日行われた potatotips という、日々の開発の Tips を共有するイベントにて未熟ながら登壇したので、今回は登壇に至った背景なども交えながらレポートします!

※ 前回は気合いだけで頑張った話を多く載せすぎてしまったので、今回はスマートにいきます

tech.dely.jp

登壇の経緯

ある日、上司と1on1をしているとこのような話になりました


私:今年の目標はスキルの向上もそうですが、社外のイベントとかコミュニティとの繋がりを増やしていきたいです

上司:うえちょさんって社外のiOSのコミュニティとかと関わりありますか?

私:iOSに関しては全く無いですね…コロナのタイミングで上京したのでイベントが全然なくて、、

上司:ならこのイベントとかどうですか?potatotipsというイベントで、LTで登壇とかもできるみたい。とりあえず今日あるみたいだから見てみたらどうですか?

私:お、気になります!見ます!

数週間後の1on1にて…

上司:うえちょさん、potatotipsの次の開催が決まったみたいだけど、登壇とかしてみるのはどうですか?

私:はい、やります 🔥🔥
(最近HealthKitとCoreMotionの開発していて躓いていたのでその話をしようかな)


という流れであっさり決まってしまったイベント登壇でした。
私自身iOSDCなど大きめのカンファレンスやイベントは見るようにしていたのですが、小規模な(とはいえ参加者が100人近くいる、東京凄い)イベントは全然知らなかったこともあり、ワクワクしながらイベントを見ました。

発表内容 - HealthKit と CoreMotion の権限に四苦八苦した話

クラシルリワードのiOSアプリでは歩数に応じて歩数ゲージが蓄積され、蓄積された歩数ゲージをチケットに交換することができる機能があります。この機能を実現するために利用しているのが HealthKit と CoreMotion です。

この HealthKit と CoreMotion の取得したデータを利用するには、OS側の制限によりユーザーからの許可が必要です。この権限の許可率は、歩数機能をどのくらいのユーザーが利用してくれるかを示す重要な指標であるため、できる限り正確なデータの取得が求められていました。

私がクラシルからクラシルリワードへ移動したタイミングは、この許諾率を向上させるための施策が実施されていたため、これまで経験のなかった HealthKit と CoreMotion に初めて取り組むことになりました。

まずは大枠を掴もうということで、それぞれの権限状態とミニマムな取得方法ついてまとめました。

CoreMotionの権限状態と取得方法

CoreMotion は iOS4.0 から利用できる、かなり古くからあるフレームワークです。加速度やジャイロスコープなどアプリが入っている端末のセンサーが取得したデータを利用する際や、ユーザの活動タイプ(歩行、自転車など)を特定したりできます。データを取得するにはユーザの許可が必要です。

CoreMotionでは以下の4つの状態が存在します。これらは CMAuthorizationStatus として定義されています。

CMAuthorizationStatusの4つの許可状態

  • notDetermined:ユーザーがアプリにモーションデータへのアクセスを許可・否定どちらもしていない状態。
  • authorized:ユーザーがアプリにモーションデータへのアクセスを許可している状態。
  • denied:ユーザーがアプリにモーションデータへのアクセスを拒否している状態。
  • restricted:ペアレンタルコントロールなど、何らかの制限によりアプリがモーションデータへのアクセスを要求できない状態。

CoreMotion の許可状態は比較的簡単に、素直に取得することができます。

// CoreMotionの許可状態の取得方法

import CoreMotion

let status = CMMotionActivityManager.authorizationStatus()

switch status {
case .notDetermined:
    print("🐶< ユーザは許可も拒否もしていないわん")
case .authorized:
    print("🐶< ユーザはアクセス許可しているわん")
case .restricted:
    print("🐶< アクセスが制限されているわん")
case .denied:
    print("🐶< ユーザによってアクセスが拒否されたわん")
}

HealthKit の権限状態と取得方法

HealthKit は iOS8.0 から利用できる、それなりに古いフレームワークです。ヘルスケアアプリに保存されるデータ(健康関連データ)を利用することができます。データを取得するにはユーザの許可が必要です。

HealthKit の大きな特徴として、iPhoneだけでなく、任意のデバイス(AppleWatch等)が取得したデータも一元管理しているので、アプリは特に意識することなく任意デバイスが収集したデータも利用できます。

HealthKit では以下の3つの状態が存在します。これらは HKAuthorizationStatus として定義されています。

HKAuthorizationStatus の3つの権限状態

  • notDetermined:ユーザがアプリに特定の健康データへのアクセスを許可・拒否どちらもしてない状態(アクセス依頼を受けていない状態)
  • sharingAuthorized:アクセスが許可されている
  • sharingDenied:アクセスが拒否されている

HealthKit の許可状態も同様に取得してみるため以下のようにコードを書きましたが、notDetermined以外の状態が正しく取得できません。

// HealthKit の許可状態の取得方法
import HealthKit

let store = HKHealthStore()
let status = store.authorizationStatus(for: HKQuantityType(.stepCount))

switch status {
case .notDetermined:
    print("🐶< ユーザは許可も拒否もしてないわん")
case .sharingDenied:
    print("🐶< ???")
case .sharingAuthorized:
    print("🐶< ???")
}

Appleは HealthKit の許可状態に関してもプライバシーとして教えてくれないとのことです。

To help maintain the privacy of sensitive health data, HealthKit does not tell you when the user denies your app permission to query data.

developer.apple.com

しかしこれだと施策をするうえで非常に困ってしまうので解決方法を模索することにしました。

HealthKit の許可状態を擬似的に取得する

色々と試した結果、実際に数値を取得してみて、取得できる→許可されている、取得できない→許可されていない、と判断することで権限状態を擬似的に取得できることが判明しました。

notDetermined の状態は取得できるので、そもそも権限付与依頼のモーダルを出していない場合は出すようにします。notDetermined以外の場合は、実際に数値を取得します。この際、 HKError が発生した場合は拒否されている可能性が高く、それ以外のエラータイプ場合は許可されていると判断できます。

// HealthKit の許可状態を擬似的に取得する
import HealthKit

let store = HKHealthStore()
let status = store.authorizationStatus(for: HKQuantityType(.stepCount))

if status == .notDetermined {
    print("🐶< まずはユーザに許可をもとめるわん")
} else {
    do {
        _ = try await 今日の歩数を取得する関数() // 実際に取得しようとする
        print("🐶< 今日の歩数が取得できたのでアクセスは許可されているわん")
    } catch let error as HKError {
        switch error.code {
        case .errorAuthorizationDenied, .errorAuthorizationDenied, .errorRequiredAuthorizationDenied:
            print("🐶< アクセスは拒否されているわん")
        default:
            print("🐶< データは取得できないけど許可されているわん")
        }
    } catch {
        print("🐶< データは取得できないけど許可されているわん")
    }
}

この際、歩数が0歩の場合もエラーになるので、細かくエラーハンドリングする必要があります。

まとめ

CoreMotionとHealthKitの許可状態の取得まとめ

CoreMotion は何ら問題なく権限状態が取得できますが、HealthKitでは少しひねって取得する必要がありました。

参考

HealthKitの開発時にはこちらの記事が参考になりました: https://qiita.com/dotrikun/items/f34420cb7f3c0fb2ac09