iOS15で追加されたHealthKitのrequestAuthorizationを試す

開発環境

> xcodebuild -version
Xcode 13.3
Build version 13E113

モチベーション

  • Swift Concurrencyに入門したい
  • とりあえず個人開発中のサイクリングログViewerアプリ(tokizuoh/contrail)で実験

iOS15で追加されたHealthKitのrequestAuthorization

iOS15からHealthKitのAPIrequestAuthorization(toShare:read:)が追加された。
それまではワークアウトデータの取得前にrequestAuthorization(toShare:read:completion:)を使用してユーザーに認可リクエストを行う必要があり、認可処理が終わった後の処理をクロージャとして渡す必要があった。iOS15から追加されたAPIはSwift Concurrency対応がされており、非同期的にコードを書くことができる。

(特に大きな問題は無いが、iOS15から追加されたAPIは引数のSetがnil許容でなくなっている)

コード

コード全部書くと見にくいのでGitHubのPRを添付。

github.com

細かいコメントはPRページに書いてあるが、特にGoodなところを下記に記載。

ネストが減ってより分かりやすく見やすくなった

今までは非同期処理を同期的に書く際はクロージャを利用していたため、クロージャ宣言側のネストが深くなりコードの複雑性が上がるにつれて可読性が悪くなっていった。

import Foundation

func greet(_ something: String, completion: () -> Void){
  print("Hello, " + something + "!")
  completion()
}

greet("Soshina") {
  // このブロック
  print("いや誰やねん!")
}

Swift Concurrency対応のAPIを用いることで、よりスッキリ書くことができた。

Before

let readTypes = Set([
    HKObjectType.workoutType()
])
healthStore.requestAuthorization(toShare: nil, read: readTypes) { _, error in
    guard error == nil else {
        // エラー処理
        return
    }
}

After

let readTypes = Set([
    HKObjectType.workoutType()
])
do {
    try await healthStore.requestAuthorization(toShare: Set([]), read: readTypes)
} catch let error {
     // エラー処理
}

今回でSwift Concurrencyに入門したがまだ手に馴染んでいないので何か題材を見つけて書いていく。

参考

Xcodeエラー対応: ld: symbol(s) not found for architecture x86_64

開発環境

> xcodebuild -version
Xcode 13.3
Build version 13E113

エラー

Undefined symbols for architecture x86_64:
  "SampleModule.SampleModule.init() -> SampleModule.SampleModule", referenced from:
      Test20220519.ViewController.viewDidLoad() -> () in ViewController.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

エラー再現手順

  1. XcodeのProject内にPackageを追加
  2. Project側で1で追加したPackageの処理を利用
  3. Project実行時に「ld: symbol(s) not found for architecture x86_64」が発生

1. XcodeのProject内にPackageを追加

2. Project側で1で追加したPackageの処理を利用

Package側

public struct SampleModule {
    public private(set) var text = "Hello, World!"

    public init() {
    }
}

Project側

import UIKit
import SampleModule

final class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        print(SampleModule().text)
    }
}

この時点でコード補完は効く。

3. Project実行時に「ld: symbol(s) not found for architecture x86_64」が発生

Undefined symbols for architecture x86_64:
  "SampleModule.SampleModule.init() -> SampleModule.SampleModule", referenced from:
      Test20220519.ViewController.viewDidLoad() -> () in ViewController.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

対応方法

XcodeのTARGETS > Frameworks, Libraries, and Embedded ContentにPackageを追加する。

追加された状態になるとProjectの実行が成功する。

HealthKitでcyclingの時速は直接取得できないぞ!

結論

HealthKit経由で取得したワークアウト(HKWorkout) から時速を取得するには、 totalDistance(HKQuantity)duration(TimeInterval) を使って算出する必要がある。

let distance = workout.totalDistance!.kilometers()
let averageSpeed = distance / ((Double)(Int(timeInterval)) / 3600)

extension HKQuantity {
    func kilometers() -> Double {
        return self.doubleValue(for: .meter()) / 1000
    }
}

開発環境

> xcodebuild -version
Xcode 13.3
Build version 13E113

モチベーション

最近趣味でApple Watchで計測したサイクリングのワークアウトのデータを集計するSwiftUI製アプリを作っている。

github.com

現状の問題として、1回のワークアウトの合計走行距離や時間はHealthKit経由で取得できたが時速を取得できない。時速を取得したい。

勘違い

Apple公式のフィットネスのアプリでワークアウトを見ると、平均速度の項目があるので合計走行距離(totalDistance(HKQuantity)などと同様にHealthKit経由で取得できると考えていた。
しかし、いくらドキュメントを彷徨っても該当するプロパティが存在しない。
一番近そうなHKMetadataKeyAverageSpeedはサイクリングのワークアウトのmetaDataにKeyが存在せず、権限周りに問題があるかと思ったが結果として取得できなかった。

結果として、結論にも書いてあるが合計走行距離とワークアウトにかかった時間は既に取得できていたので、それらを使って計算させればいいだけのことだった。(ということに気づくのに時間がかかった)

コード

let distance = workout.totalDistance!.kilometers()

// TimeIntervalの単位は秒のため時速を計算するために時間(hour)に直す
let averageSpeed = distance / ((Double)(Int(timeInterval)) / 3600)

extension HKQuantity {
    // 標準でHKQuantityをメートルに変換する処理があるので利用。km/hを計算したいのでmをkmに変換する処理を追加。
    func kilometers() -> Double {
        return self.doubleValue(for: .meter()) / 1000
    }
}

TimeIntervalはDoubleのtypealiasなんだね。知らなんだ。

参考