🐙

apollo-iosでGitHub GrqphQL APIを利用する

に公開

概要

  • GitHubAPIはREST APIの他にGraphQL APIが用意されています
  • ピン留めされたリポジトリや草の情報の取得など、GraphQLにしかない機能もあるみたいです
  • 今回GraphQLのクライアントライブラリであるapollo-iosを使って、簡単なレポジトリの検索まで実装します
  • 注意する点としては、現状ライブラリがSwift6やSwift Concurrency(こちらはWrapperで対応はできそう)に非対応で、v2.0まで待たないといけないようです
  • なおREST APIのようにURLSession/URLRequestを使ってスクラッチで書けはしますが、クエリの作成やレスポンス型の定義などの実装により手間がかかるので、ライブラリを使うのが良いのかなと思っています

GitHub

参考

apollo-iosのライブラリを追加

https://github.com/apollographql/apollo-ios.git

  • このとき下記のライブラリのみ選択すること

GitHub GraphQL APIのSchemaファイルの追加

  • GitHub GraphQL APIのデータや構造の情報が記載されたSchemaファイルが用意してあるので、下記からダウンロードしてプロジェクトのフォルダに追加します
  • また後述のGraphQL操作ファイルの.graphqlと拡張子が被ってしまう都合上、ファイルの拡張子を.graphqlsに変更しておくと分かりやすいと思います
schema.docs.graphql
↓
schema.docs.graphqls

GraphQLクエリの作成

  • 拡張子.graphqlファイルを作成し、実装したいAPIの操作を示したGraphQLクエリを記載します
  • 今回SearchRepos.graphqlというファイルを作成し、下記の内容を記載します
  • (内容に関しては後ほど補足します)
query SearchRepos($query: String!, $first: Int = 10, $after: String) {
  search(
    after: $after
    first: $first
    query: $query
    type: REPOSITORY
  ) {
    repositoryCount
    edges {
      cursor
      node {
        ... on Repository {
          id
          name
        }
      }
    }
  }
}

apollo-iosの設定ファイルの作成

  • 下記を選択して実行します

  • すると下記のCLIファイルが作成されます
  • (現状Xcode 16.3ではバグがあり作成されないため、16.2に下げるなどが必要です: 参考)

  • 続いて作成されたCLIファイルを使って、apollo-iosの設定ファイルを作成します
cd <path to dir>

./apollo-ios-cli init \
--schema-namespace GitHubGraphQLAPI \
--module-type embeddedInTarget \
--target-name ApolloDemo
  • apollo-codegen-config.jsonが作成されます
{
  "schemaNamespace" : "GitHubGraphQLAPI",
  "input" : {
    "operationSearchPaths" : [
      "**/*.graphql"
    ],
    "schemaSearchPaths" : [
      "**/*.graphqls"
    ]
  },
  "output" : {
    "testMocks" : {
      "none" : {
      }
    },
    "schemaTypes" : {
      "path" : "./GitHubGraphQLAPI",
      "moduleType" : {
        "embeddedInTarget" : {
          "name" : "ApolloDemo"
        }
      }
    },
    "operations" : {
      "inSchemaModule" : {
      }
    }
  }
}
  • それぞれの設定の意味は下記の通りです
  • Schemaファイルの対象や、作成したコードの配置場所など任意の設定をここで行えます
    • なお前にschema.docs.graphqlsと拡張子を変えていたのもこのためですね

apollo-iosによるコードの生成

  • 先ほどの設定が完了したら、CLIを使ってコードを作成します
./apollo-ios-cli generate
  • 下記の通り指定したファイルが出力されていればOKです

  • 作成されたコードを実際に使ってみます。
  • 設定ファイルで指定した名前空間のstaticメソッドから、定義したクエリのリクエストを作成できるようになっています
// クエリの作成
let query = GitHubGraphQLAPI.SearchReposQuery(query: "SwiftUI", after: nil)
  • そしてこのクエリをApolloClientに渡して実行していくのですが、現在apollo-iosがSwift Concurrencyに非対応なのでWrapperを書いていきます

ApolloClientのSwift Concurrency対応

enum ApolloClientCustomError: Error {
    case isResultDataNil
}

extension ApolloClientCustomError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .isResultDataNil:
            return "Result data is nil"
        }
    }
}
extension ApolloClient {
    public func fetch<Query: GraphQLQuery>(
        query: Query,
        cachePolicy: CachePolicy = .default,
        contextIdentifier: UUID? = nil,
        queue: DispatchQueue = .main
    ) async throws -> Query.Data {
        return try await withCheckedThrowingContinuation { continuation in
            fetch(
                query: query,
                cachePolicy: cachePolicy,
                contextIdentifier: contextIdentifier,
                queue: queue
            ) { result in
                do {
                    let result = try result.get()
                    if let data = result.data {
                        continuation.resume(returning: data)
                    } else {
                        continuation.resume(throwing: ApolloClientCustomError.isResultDataNil)
                    }
                } catch {
                    continuation.resume(throwing: error)
                }
            }
        }
    }
}

GitHub APIの認証を行いアクセストークンを取得

ApolloClientの作成

https://www.apollographql.com/docs/ios/tutorial/tutorial-execute-first-query

  • 今回アクセストークンの変更時にApolloClientを都度作成し直すようなことをしています。
  • ただ公式式ドキュメントに認証情報の同期の方法が書いてあり、正式にはこちらの書き方が適しているかもしれません
extension ApolloClient {
    static func create(accessToken: String? = nil) -> ApolloClient {
        let cache = InMemoryNormalizedCache()
        let store = ApolloStore(cache: cache)
        let client = URLSessionClient()
        let provider = DefaultInterceptorProvider(client: client, store: store)
        let url = URL(string: "https://api.github.com/graphql")!
        
        let headers: [String: String] = if let accessToken {
            ["Authorization": "Bearer \(accessToken)"]
        } else {
            [:]
        }
        
        let transport = RequestChainNetworkTransport(
            interceptorProvider: provider,
            endpointURL: url,
            additionalHeaders: headers
        )
        return ApolloClient(networkTransport: transport, store: store)
    }
}

Apolloクライアントからクエリの実行

  • これでようやくApolloクライアントからクエリの実行ができます
  • 返り値の型もクエリで定義したものが返ってきており、きちんと取得できている事がわかります
let query = GitHubGraphQLAPI.SearchReposQuery(query: "SwiftUI", after: nil)
let data = try await apolloClient.fetch(query: query)
let repos =  data.search.edges?.compactMap { $0?.node?.asRepository } ?? []
for repo in repos {
    print("id: \(repo.id), name: \(repo.name)")
}
id: MDEwOlJlcG9zaXRvcnkxOTAyMTE2NjM=, name: SwiftUI
id: MDEwOlJlcG9zaXRvcnkxOTAwODIzNjg=, name: SwiftUI
id: MDEwOlJlcG9zaXRvcnkxOTAxNDk0OTM=, name: swiftui
id: MDEwOlJlcG9zaXRvcnkxOTAxODk3MDI=, name: SwiftUIX
id: MDEwOlJlcG9zaXRvcnkyMTgwODgwOTM=, name: clean-architecture-swiftui
id: MDEwOlJlcG9zaXRvcnkxOTA3MDc2MTM=, name: About-SwiftUI
id: MDEwOlJlcG9zaXRvcnkyMjQzMDY2NTg=, name: swiftui-introspect
id: MDEwOlJlcG9zaXRvcnkxOTE2MjU4MTc=, name: ChartView
id: R_kgDOIeGPCw, name: IceCubesApp
id: MDEwOlJlcG9zaXRvcnkxOTEzNTE3Njc=, name: open-swiftui-animations
  • apollo-iosの使い方としては以上です!

GraphQLのクエリの作り方

  • 先ほどGraphQLの詳しい作り方は省略していたので、以下その補足です
  • まずGraphQLのクエリの作成に関しては、それ用のツールを使うと良いと思います
  • 私はAltair GraphQL Clientを利用しています
  • ドラッグ・アンド・ドロップなどでGraphQL APIのSchemaファイルであるschema.docs.graphqlsをインポートすることでドキュメントが作成されます
  • このドキュメントをみながらクエリを作成していくことになります

  • 今回はsearchを使ってリポジトリを検索することを考えます

  • ドキュメントを見ながら、必要なパラメータや取得したい情報を組み立てます(文法は割愛します)
query SearchRepos($query: String!, $first: Int = 10, $after: String) {
  search(
    after: $after
    first: $first
    query: $query
    type: REPOSITORY
  ) {
    repositoryCount
    edges {
      cursor
      node {
        ... on Repository {
          id
          name
        }
      }
    }
  }
}
  • URLやクエリ変数は下記の通り設定できます

  • また認証情報が必要な場合があるので、ヘッダにアプリで取得したアクセストークンを追加しておくと便利です

  • 以上を実行すると成功レスポンスが返ってきて、うまく動作していることがわかります

  • また取得する型を共通にしたいと思うので、その際はfragmentを定義して対応できます
  • 下記を例にすると、取得したいリポジトリの情報をRepositoryFieldsとして定義して、これを複数のクエリで利用しています

Discussion