この記事は、JapanTaxi Advent Calendar 2018の25日目の記事です。
JapanTaxi iOSアプリは、アーキテクチャ変更と同時にAPI通信もRESTからGraphQLに移行し始めました。(アーキテクチャ変更については、JapanTaxi iOSアプリにRIBsアーキテクチャを導入して得られたことをご覧ください。) この記事では、GraphQLへ移行したことによるメリットや、Apollo iOSを利用した実装方法について紹介します。
GraphQLを導入することでクライアントサイドにおけるメリットは以下です。
GraphQLのフォーマットで書かれたクエリがそのままAPI通信の定義となるので実装が簡潔になります。graphiql(詳細は後述)を使えば、通信結果の確認もしやすくなります。また、iOSとAndroidなど複数のクライアントがある場合に定義を共通化しやすくなります。
サーバサイドとクライアントサイドで微妙に型の名前が異なるといった場面はよくあります。GraphQL上で定義された型はサーバサイドでもクライアントサイドでも共通なので、型名の差が生まれにくくなります。
RESTだとエンドポイントごとに返されるデータが決まっているため、ひとつの画面を作り上げるために、場合によっては複数のAPIと通信する必要がありました。GraphQLだとエンドポイントは一つしかなく、クエリで必要な情報をまとめて記述することができます。たとえば、ユーザ情報と注文データの一部を1回のリクエストで取得することができるようになります。クライアントサイドで必要な情報を自由に組み合わせられるのは非常に便利です。
Apollo iOSというGraphQLクライアントがあります。JapanTaxi iOSアプリではこのライブラリを利用して実装しました。 以下に、Apollo iOSの概要について紹介します。
Apollo iOSを利用すると、GraphQLのスキーマ(型などの定義)とクライアントサイドで作成したクエリを元に、API通信に必要なリクエストおよびレスポンスのクラス(Swift)を自動生成してくれます。APIから返ってきたJSONのパース処理などもすべて行ってくれます。API通信を行うにあたって、クライアントサイドで必要な実装は「クエリの定義」のみになります。
Apollo iOSの公式チュートリアルをもとに、プロジェクトに取り込みましょう。CocoaPodsやCarthageに対応していますし、直接マニュアルで導入することもできます。
ライブラリを導入後、Adding a code generation build stepを参考に、プロジェクトの「Build Phases」にスクリプトを仕込みましょう。「check-and-run-apollo-cli.sh」というスクリプトが、ビルドをするたびに「API.swift」というファイルを生成します。API.swiftは、API通信を行う際に必要なリクエストとレスポンスの定義をひとまとめにしたファイルです。
API.swiftを生成する際に、以下のファイルが必要になります。
– xxxxxx.graphql
リクエスト情報を扱うファイル。GraphQLのフォーマットに従ってクエリを記載する。
– schema.jsonGraphQLの定義をまとめたファイル。サーバサイドで管理されている。
graphqlファイルは、API通信のリクエストを定義する際にクライアントサイドで都度作成しましょう。schema.jsonは、サーバサイドでGraphQLの定義が変更された場合に、都度ダウンロードが必要です。schema.jsonのダウンロード方法についてはDownloading a schemaで説明されているのでご参考ください。
Swiftのコードをまじえて、Apollo iOSによる通信処理を紹介します。
GraphQLでリクエストのクエリを書くと以下のようになります。ユーザ情報と指定したキーワードで検索した結果を同時に取得するクエリです。
fragment RepositoryDetail on Repository {
name
url
}
query Search($query: String!) {
viewer {
avatarUrl
login
name
starredRepositories(last: 1) {
edges {
node {
...RepositoryDetail
}
}
}
}
search(first:10, query:$query, type: REPOSITORY) {
edges {
node {
...RepositoryDetail
}
}
}
}
クライアントサイドで別の型として定義させたい場合は「Fragment」を利用しましょう。クエリには引数を設けることができ、上記の例だと検索するキーワードをSwiftのコードから指定させることができます。このクエリを「Search.graphql」のように、.graphqlを拡張子としたファイルでプロジェクト内に保存します。
先ほどのクエリをなげると、GraphQLからは以下のような結果が返ってきます。
{
"data": {
"viewer": {
"avatarUrl": "https://avatars0.githubusercontent.com/u/1837344?v=4",
"login": "imairi",
"name": "Yosuke Imairi",
"starredRepositories": {
"edges": [
{
"node": {
"name": "SnapLikeCollectionView",
"url": "https://github.com/kboy-silvergym/SnapLikeCollectionView"
}
}
]
}
},
"search": {
"edges": [
{
"node": {
"name": "swift",
"url": "https://github.com/apple/swift"
}
},
… 略 …
{
"node": {
"name": "SwiftLanguageWeather",
"url": "https://github.com/JakeLin/SwiftLanguageWeather"
}
}
]
}
}
}
Apollo iOSを利用して、指定したクエリをサーバサイドへリクエストし、その結果を受け取るためには以下のように実装すればよいです。
let configuration: URLSessionConfiguration = .default
configuration.httpAdditionalHeaders = ["Authorization": "Bearer xxx_token_xxx"]
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
guard let url = URL(string: "https://api.github.com/graphql") else {
return
}
// Apolloのクライアント作成
let apollo = ApolloClient(networkTransport: HTTPNetworkTransport(url: url, configuration: configuration))
// リクエスト
apollo.fetch(query: SearchQuery(query: "swift"), resultHandler: { (result, error) in
switch (result?.data, result?.errors, error) {
case (.some(let data), .none, .none): // 成功
// ユーザ情報
let viewer = data.viewer
print("User avatarUrl: \(viewer.avatarUrl)")
print("User login: \(viewer.login)")
print("User name: \(viewer.name ?? "")")
// スターつけたリポジトリ
let starredRepositories = viewer.starredRepositories.edges?.compactMap{ $0?.node.fragments.repositoryDetail }
starredRepositories?.forEach {
print("Repository name: \($0.name)")
print("Repository url: \($0.url)")
}
// 検索結果
let repositories = data.search.edges?.compactMap{ $0?.node?.fragments.repositoryDetail }
repositories?.forEach {
print("Repository name: \($0.name)")
print("Repository url: \($0.url)")
}
case (.none, .some(let errors), _): // リクエストエラーや一部のビジネスロジクでのエラーなど
errors.forEach {
print("GraphQL error", $0.description)
}
case (.none, .none, .some(let error)): // サーバエラーなど
print("GraphQL error", error)
default:
break
}
})
まずURLSessionConfigurationを作成し、HTTPヘッダーやキャッシュポリシーなどの設定をします。それを元にApolloClientを生成し、fetchメソッドでAPIへリクエストすることができます。
ApolloClientには「fetch」と「perform」という通信用メソッドが用意されており、fetchはデータの参照時に利用し、performは更新時に利用します。
ApolloClientによるレスポンスは成功パターンと失敗パターンの2種類に分けられます。上記の「result」が成功した場合に含まれるデータで、「error」はその名のとおりサーバエラーなどが発生した場合に返ってきます。 RESTと違う点としては、resultの中にも「errors」というエラー情報が含まれることです。GraphQLではリクエストが通れば結果は200で返ってきます。たとえば、上記のクエリを例に取ると、ユーザ情報は正常に取得できたが、検索へのリクエスト情報に不備があった場合は、200が返ってきて result?.errors にエラー情報が含まれます。一方、リクエストが正常に受け取られなかった場合、たとえば権限エラーやサーバエラーなどは error にエラー情報が含まれます。このあたりのエラーハンドリングはRESTと異なるので注意が必要です。
Xcode上でGraphQLのファイルを扱う際、xcode-graphqlというプラグインを入れることでシンタックスハイライトが効くようになります。
右側のように、プラグインを導入するとクエリが読みやすくなりました。
GraphQLのリクエストとレスポンスをブラウザ上で確認するには「GraphiQL」が便利です。
GitHub GraphQL APIで試すことができるので、GraphQLがどんなものか知りたい方はぜひご覧ください。GraphQLのリクエストを作成する際や、レスポンスの型や詳細の情報を調べる際に役立ちます。
graphdocを利用すれば、GraphQLの型一覧をブラウザで閲覧できるようになります。GraphQLのエンドポイントを指定してもよいですし、schema.jsonからも生成可能です。
ローカルで生成して自分用に使ってもよいですし、サーバサイドで定期的に生成するようにすれば最新情報が常に閲覧できるので便利ですね。型を絞り込み検索できるところが助かります。
GraphQLのクラスをJSON形式に変換したり、その逆を行うことができます。GraphQLのクラスをJSON形式にするには以下のように「jsonObject」プロパティを参照しましょう。
let jsonObject = result?.data.viewer.jsonObject
jsonObjectは [String : Any] 型で定義されています。
JSONを読み込んでGraphQLの型を生成するには以下のようにします。
private func loadJSON(from fileURL: URL) -> Any? {
guard let data = try? Data(contentsOf: fileURL) else {
return nil
}
return try? JSONSerialization.jsonObject(with: data, options: [])
}
func createUserMock() -> User? {
guard let fileURL = Bundle(for: type(of: self)).url(forResource: “user”, withExtension: "json") else {
return nil
}
guard let jsonObject = loadJSON(from: fileURL) as? JSONObject else {
return nil
}
return User.init(unsafeResultMap: jsonObject)
}
user.jsonに記述されたJSON形式のユーザ情報を読み込み、init(unsafeResultMap: ResultMap) でGraphQLで定義された型のオブジェクトを作成できます。主にUnitTestでモックを作成するときに活用できると思います。
RESTからGraphQLに切り替えることで、リクエスト回数を減らせたり、API通信の実装が容易になりました。まだ半分程度しか置き換えられていませんが、これからも継続的に改修を進めていきます。
Mobility Technologies では共に日本のモビリティを進化させていくエンジニアを募集しています。話を聞いてみたいという方は、是非 募集ページ からご相談ください!