Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Apollo-iOSを使用してGraphQLを叩く

こんにちは。
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に気がつかなくハマることのない世界、すばらしいですね。