Apollo iOSを利用してGraphQL APIと通信する方法

https://cdn-ak.f.st-hatena.com/images/fotolife/g/go_dev/20240418/20240418124240.jpg

この記事は、JapanTaxi Advent Calendar 2018の25日目の記事です。

はじめに

JapanTaxi iOSアプリは、アーキテクチャ変更と同時にAPI通信もRESTからGraphQLに移行し始めました。(アーキテクチャ変更については、JapanTaxi iOSアプリにRIBsアーキテクチャを導入して得られたことをご覧ください。) この記事では、GraphQLへ移行したことによるメリットや、Apollo iOSを利用した実装方法について紹介します。

GraphQLを導入するメリット

GraphQLを導入することでクライアントサイドにおけるメリットは以下です。

通信まわりの実装が簡潔になる

GraphQLのフォーマットで書かれたクエリがそのままAPI通信の定義となるので実装が簡潔になります。graphiql(詳細は後述)を使えば、通信結果の確認もしやすくなります。また、iOSAndroidなど複数のクライアントがある場合に定義を共通化しやすくなります。

サーバサイドで定義された型をそのまま利用できる

サーバサイドとクライアントサイドで微妙に型の名前が異なるといった場面はよくあります。GraphQL上で定義された型はサーバサイドでもクライアントサイドでも共通なので、型名の差が生まれにくくなります。

必要な情報をまとめて取得できる

RESTだとエンドポイントごとに返されるデータが決まっているため、ひとつの画面を作り上げるために、場合によっては複数のAPIと通信する必要がありました。GraphQLだとエンドポイントは一つしかなく、クエリで必要な情報をまとめて記述することができます。たとえば、ユーザ情報と注文データの一部を1回のリクエストで取得することができるようになります。クライアントサイドで必要な情報を自由に組み合わせられるのは非常に便利です。

Apollo iOS

Apollo iOSというGraphQLクライアントがあります。JapanTaxi iOSアプリではこのライブラリを利用して実装しました。 以下に、Apollo iOSの概要について紹介します。

なにができるのか

Apollo iOSを利用すると、GraphQLのスキーマ(型などの定義)とクライアントサイドで作成したクエリを元に、API通信に必要なリクエストおよびレスポンスのクラス(Swift)を自動生成してくれますAPIから返ってきたJSONのパース処理などもすべて行ってくれます。API通信を行うにあたって、クライアントサイドで必要な実装は「クエリの定義」のみになります。

導入方法

1. Apolloをプロジェクトに取り込む

Apollo iOSの公式チュートリアルをもとに、プロジェクトに取り込みましょう。CocoaPodsやCarthageに対応していますし、直接マニュアルで導入することもできます。

2. API.swiftを作成するスクリプトを仕込む

https://cdn-ak.f.st-hatena.com/images/fotolife/g/go_dev/20240418/20240418124239.jpg

ライブラリを導入後、Adding a code generation build stepを参考に、プロジェクトの「Build Phases」にスクリプトを仕込みましょう。「check-and-run-apollo-cli.sh」というスクリプトが、ビルドをするたびに「API.swift」というファイルを生成します。API.swiftは、API通信を行う際に必要なリクエストとレスポンスの定義をひとまとめにしたファイルです。

3. クエリとスキーマを用意する

API.swiftを生成する際に、以下のファイルが必要になります。

– xxxxxx.graphql

リクエスト情報を扱うファイル。GraphQLのフォーマットに従ってクエリを記載する。

– schema.jsonGraphQLの定義をまとめたファイル。サーバサイドで管理されている。

graphqlファイルは、API通信のリクエストを定義する際にクライアントサイドで都度作成しましょう。schema.jsonは、サーバサイドでGraphQLの定義が変更された場合に、都度ダウンロードが必要です。schema.jsonのダウンロード方法についてはDownloading a schemaで説明されているのでご参考ください。

Apollo iOSで通信処理を実装する

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と異なるので注意が必要です。

GraphQLでの開発をより便利にする

シンタックスハイライト

Xcode上でGraphQLのファイルを扱う際、xcode-graphqlというプラグインを入れることでシンタックスハイライトが効くようになります。

https://cdn-ak.f.st-hatena.com/images/fotolife/g/go_dev/20240418/20240418124241.jpg

右側のように、プラグインを導入するとクエリが読みやすくなりました。

GraphiQL

GraphQLのリクエストとレスポンスをブラウザ上で確認するには「GraphiQL」が便利です。

https://cdn-ak.f.st-hatena.com/images/fotolife/g/go_dev/20240418/20240418124238.jpg

GitHub GraphQL APIで試すことができるので、GraphQLがどんなものか知りたい方はぜひご覧ください。GraphQLのリクエストを作成する際や、レスポンスの型や詳細の情報を調べる際に役立ちます

GraphQLからドキュメントを作成する

graphdocを利用すれば、GraphQLの型一覧をブラウザで閲覧できるようになります。GraphQLのエンドポイントを指定してもよいですし、schema.jsonからも生成可能です。

https://cdn-ak.f.st-hatena.com/images/fotolife/g/go_dev/20240418/20240418124237.jpg

ローカルで生成して自分用に使ってもよいですし、サーバサイドで定期的に生成するようにすれば最新情報が常に閲覧できるので便利ですね。型を絞り込み検索できるところが助かります

モックを作る

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通信の実装が容易になりました。まだ半分程度しか置き換えられていませんが、これからも継続的に改修を進めていきます。