こんにちは。
iOSエンジニアの弘田です。
みなさんGraphQLはご存知ですか?
知らない方は弊社のエンジニアがGraphQLの記事を書いているのでぜひ読んでみてください。
GraphQLを導入しようとしている話
Studyplusのアプリで一部GraphQLを使用する際にApolloを使用しました。
Apolloって?
ApolloはGraphQLベースのデータスタックです。
環境別にライブラリがあり、今回はApollo-iOSを使用します。
https://github.com/apollographql/apollo-ios
公式サイト : https://www.apollographql.com/docs/ios/installation.html
Apollo-iOSの選定理由
- Swift4がサポートされている
- Apollo-cliでModelを生成できる
- サーバーが更新されたらクエリを書いてスクリプトを実行すればいいので人為的ミスが減る
- 詳細は後述
- サーバーが更新されたらクエリを書いてスクリプトを実行すればいいので人為的ミスが減る
GraphQLをPlaygroundから叩いてみる
まずはApollo-iOSを使用せず、普通にGraphQLのエンドポイントを叩いてみましょう。
GraphQLはクライアント側で必要な情報だけを取得するためにクエリを書く必要があります。
クエリはJSON形式で記述します。
このようなデータがあるとします。
user { id name age }
このときアプリ側で必要な情報はnameだけあれば問題ない時ありますよね。
REST APIの場合はエンドポイントが/userのようになっていて、
idやageも返却されresponse内容に使わない情報が含まれていることがよくあります。
ではGraphQLのnameだけをリクエストするクエリを書いてみましょう。
{ user { name } }
このクエリをURLRequestのbodyに入れてPOSTします。
import UIKit import PlaygroundSupport PlaygroundPage.current.needsIndefiniteExecution = true let url = URL(string: "http://localhost:3000/graphiql") var request = URLRequest(url: url!) request.httpMethod = "POST" request.addValue("application/json; charaset=utf-8", forHTTPHeaderField: "Content-Type") //認証Headerが必要な場合 request.addValue(String(format: "OAuth %@", token), forHTTPHeaderField: "Authorization") let query = "{ user { name } }" let body = ["query": query] request.httpBody = try! JSONSerialization.data(withJSONObject: body, options: []) request.cachePolicy = .reloadIgnoringLocalCacheData let task = URLSession.shared.dataTask(with: request, completionHandler: {data, reposnse, error in if let error = error { print(error); return } guard let data = data else { print("Data is missing"); return } do { let json = try JSONSerialization.jsonObject(with: data, options: []) as? [AnyHashable: Any] guard let data = json?["data"] as? [AnyHashable: [[AnyHashable: Any]]] else { return } guard let users = data["user"] else { return } users.forEach{user in guard let name = user["name"] else { return } print(name) } } catch let e { print("Parse error: \(e)") } }) task.resume()
これでnameだけの取得ができます。
ただこれだとJSONに変換して文字列でvalueの取得をする必要があり、型のない世界になってしまします。
Codableでやってもいいですが、
queryに変更があった場合都度Modelクラスを手で直す必要がありミスに気づきにくい点があまりよくありません。
Apolloを使用すれば今あげた問題点を解決することができます。
導入
CocoaPods・Carthage両方対応に対応しています。
pod "Apollo
github "apollostack/apollo-ios"
必要なファイルをApollo-cliでダウンロード&生成する
Apollo-cliでclassの生成をするのでインストール
※Apollo-cliにはnodeが必要なので入っていなければ先にnodeをインストール
npm install -g apollo
schema.jsonを配置するディレクトリに移動し、Apollo-cliのコマンドを実行
apollo schema:download --endpoint="エンドポイント"
認証Headerが必要な場合
apollo schema:download --endpoint=エンドポイント --header="Authorization: OAuth <Token>"
schema.jsonはサーバー側で用意します。
schema.jsonは後述のAPI.swiftを生成するのに必要です。
コマンドが成功したら実行したディレクトリにschema.jsonがダウンロードされます。
次にschema.jsonがあるディレクトリでAPI.swiftを生成するコマンドを実行します。
apollo codegen:generate --queries="$(find . -name '*.graphql')" --schema=schema.json API.swift
指定したディレクトリにAPI.swiftを生成したい場合
apollo codegen:generate --queries="$(find . -name '*.graphql')" --schema=schema.json ./hoge/API.swift
上記のコマンドを実行したら指定したディレクトリに空のAPI.swiftが生成されます。
次に中身を生成するために必要な.graphqlファイルを作成します。
.graphqファイルを作る
Apolloを使用してGraphQLを叩く場合Queryは文字列でなく.graphql拡張子のファイルを作成してqueryを記述します。
一つのQueryに対して一つの.graphqlファイルを用意します。
⌘ + NでEmptyを選択し今回はnameを取得するファイルなのでName.graphqlとします。
※.graphqファイルはAPI.swfit生成コマンドを実行するディレクトリ配下に配置します。
query Query名 { 取得する情報のkey }
今回は全ユーザーのユーザー名を取得するQueryです。
query getAllUserName { user { name } }
.graphqlファイルを作成したらもう一度API.swiftの生成コマンドを実行します。
実行すると先ほど作成したName.graphqlを読み込んでAPI.swiftの中身が生成されます。
API.swiftが生成されるときに.graphqlファイルの[クエリ名Query]というclassが生成されます。
例) GetAllUserNameQuery
Apolloを使用してGraphQLを叩く
let configuration: URLSessionConfiguration = .default configuration.httpAdditionalHeaders = ["Content-Type": "application/json; charaset=utf-8", "Authorization": String(format: "OAuth %@", <TOKEN>)] configuration.requestCachePolicy = .reloadIgnoringLocalCacheData let apiPath: String = String(format: "エンドポイント") let url = URL(string: apiPath) let apollo = ApolloClient(networkTransport: HTTPNetworkTransport(url: url!, configuration: configuration,sendOperationIdentifiers: false)) //queryはapollo codegen:generateコマンドで生成されたGetAllUserNameQueryを指定 apollo.fetch(query: GetAllUserNameQuery, resultHandler: {(result, error) in guard let error = error else { print(error) return } //Userクラスの配列 guard let users = result?.data?.name as? [GetAllUserNameQuery.Data.User] else { return } print(users[0].name) })
まとめ
手順が多く感じられるかと思いますが、schema.jsonとAPI.swiftの更新をスクリプトで行なっています。
なので弊社での更新手順はこのようになります。
1. サーバー側の更新
2. .graphqファイルの作成 & クエリの記述
3. スクリプト実行
Q.XcodeのRun Scriptではなくなぜスクリプトファイルなのか。
A.担当以外のエンジニアがビルドした際に担当外のファイルに変更が入るのを防ぐため
スクリプトはGistで公開しています。
https://gist.github.com/hirota-ryo/ebb429c57db25f99a3bd584621fa81b5
感想
初めはとっつきにくさを感じましたが、実際に手を動かしてみると便利だと感じました。
Apolloを使用することで型のない世界から型のあるGraphQL実現できました。
keyの指定でtypoに気がつかなくハマることのない世界、すばらしいですね。