HKWorkoutをGraphQLで扱うためにスキーマ設計をやった

モチベーション

github.com

  • 趣味開発のHealthKitを使ったiOSアプリから外部にワークアウトのデータを保存したい
  • 将来的にiOS以外のクライアントでデータを取得したい

構築するAPIをGraphQL APIとして進める際にスキーマ定義が必要なので設計の手順をログとして残す。

手順

  1. HKWorkoutから欲しいプロパティを選ぶ
  2. フィールドの型を決める
  3. schema.graphqlsを書く

1. HKWorkoutから欲しいプロパティを選ぶ

HKWorkoutを見るとiOS16でDeprecatedになるプロパティがいくつかあるが今回はiOS15までで取得できるプロパティから選ぶ。

  • duration(TimeInterval)
  • totalDistance(HKQuantity)
  • startDate(Date)
  • workoutActivityType(HKWorkoutActivityType)

また、データ更新の際にサーバーサイド側でiOSアプリから送られてきたデータとGraphQL APIサーバーで保持する値の識別を行いたいのでIDを付与する。

2. フィールドの型を決める

duration(TimeInterval)

SwiftのTimeIntervalの実体はDouble*1。GraphQLの浮動小数点型であるFloatを使う。

totalDistance(HKQuantity)

import HealthKit

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

アプリ側で上記のようにDoubleに変換している。GraphQLの浮動小数点型であるFloatを使う。

startDate(Date)

日付を扱うスカラ型がデフォルトで定義されていないため、カスタムで定義する必要がある。スキーマ定義に中身は実装しないので実装については後続のブログで書く。

workoutActivityType(HKWorkoutActivityType)

HKWorkoutActivityTypeはワークアウトの種別を表す。趣味開発ではサイクリングとランニングの情報を持たせたいのでGraphQLのenumを利用する。GraphQLではenumeration typesというみたい。

Schemas and Types | GraphQL > enumeration-type

id

スカラ型にIDがあるので定義を見る。

ID: The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache. The ID type is serialized in the same way as a String; however, defining it as an ID signifies that it is not intended to be human‐readable.
(from: https://graphql.org/learn/schema/#scalar-types)

  • オブジェクトの再フェッチやキャッシュキーとして利用される
  • ただし、IDとして定義することは、人間が判読できるように意図されていないことを意味する

具体的な定義の仕方は定まっていないっぽい。慣例的に {型名}:{内部ID}base64エンコードしたものをIDと定義することが多いらしい。クライアント側ではIDの生成ロジックに関心を持たせずに、サーバー側で生成ロジックを持つようにする。このあたりはまだ良くわかっていないので後々変更するかも。

3. schema.graphqlsを書く

scalar Date

enum WorkoutType {
  cycling
  running
}

type Workout {
  id: ID!
  distance: Float!
  duration: Float!
  startDate: Date!
  type: WorkoutType!
}

type Query {
  workouts: [Workout!]!
}

input WorkoutInput {
  distance: Float!
  duration: Float!
  startDate: Date!
  type: WorkoutType!
}

type Mutation {
  createWorkout(input: WorkoutInput!): Workout!
}

input型のWorkoutInputにID型のフィールドが無いのはサーバーサイド側でIDを生成し、クライアントがMutation実行時にIDを知る必要が無いため。(ほんと?現状必要にならなそうだが必要になったら考える)

おわりに

後から見返せるように趣味開発リポジトリschema.graphqls のリンクを置いておく。

github.com

nodeとかよく分かっていない。GitHubのGraphql APIを参考にするなどして、今後必要になったら調べれば良さそう。

参考