🐙
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
参考
- Getting Started with Apollo iOS - Apollo GraphQL Docs
- iOS | Apollo を使って GraphQL を使えるようにする
- Apollo iOS v1.x系の変更でインパクトがある点をおさらいする - Speaker Deck
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対応
- 必要なカスタムエラーを追加し、下記の通りcompletionをasyncの形に変換します
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のissuesで紹介されているコードがあったので、こちらを利用しても良さそうです
GitHub APIの認証を行いアクセストークンを取得
- アクセストークンの取得はGraphQL APIではなくREST APIで行います
- 実装に関しては以下拙記事を参照ください
ApolloClientの作成
- クエリを実行するための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