この記事は every Tech Blog Advent Calendar 2023 の 14 日目です。
DELISH KITCHEN iOSアプリの開発を担当しています久保です。
開発中のアプリでGraphQLを利用する機会があったので、導入と利用方法についてご紹介します。
なお、GraphQLについての紹介は、今更感があるので割愛させていただきます。
ライブラリの選定
GraphQLはcurlなどで実行してもらうとわかるのですが、単なるPOSTリクエストなので、そちらで書く方法もあります。その場合ライブラリの導入は不要になりますが、レスポンスに対応するオブジェクトの自動生成などのメリットを享受できないので見送りました。
今回Android側も同様に実装する必要があり、Androidにも同じようにライブラリを提供しているという理由から、apollo-ios を採用しました。
前提条件
- Xcode 15.0.1
- Apollo 1.7.1
- ローカルに作成したパッケージから apollo-ios を利用してデータを取得します。
- データ取得先としては star-wars-swapi を使用させていただきました。
データ取得までの流れ
今回は以下の順で作業しました。
- ローカルパッケージの作成
- apollo-iosの導入
- apollo-ios-cliを用いて必要なファイルを生成する
- データを取得する部分の実装
ローカルパッケージの作成
GraphQLSample
という名称でプロジェクトを作成後、ルート直下にPackages
というGroupを作成し、その下にAPI
というパッケージを作成しました。
apollo-iosの導入
先ほど作成したPackage.swiftに依存関係を追記します。
import PackageDescription let package = Package( name: "API", products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "API", targets: ["API"]), ], dependencies: [ .package( url: "https://github.com/apollographql/apollo-ios.git", .upToNextMajor(from: "1.0.0") ), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( name: "API", dependencies: [ .product(name: "Apollo", package: "apollo-ios"), ] ), .testTarget( name: "APITests", dependencies: ["API"]), ] )
apollo-ios-cliを用いて必要なファイルを生成する
appollo-ios-cli
コマンドを利用可能な状態にします
Sources配下にbinフォルダを作成し、そこに以下コマンドで生成します
$ swift package --allow-writing-to-package-directory apollo-cli-install
なお、実行可能なファイルを含んだままApp Store Connectにアップロードしようとするとエラーになるため、適宜シンボリックリンクに変更するか、ビルド時に除外するなりの対応が必要です。
設定ファイルを作成する
ここが個人的に一番面倒でした...まずコマンドを利用して apollo-codegen-config.json
を生成します
$ bin/apollo-ios-cli init --schema-namespace SW --module-type embeddedInTarget --target-name API
schemaをダウンロードするための設定を追加&取得
生成された設定ファイルに追記します
"schemaDownloadConfiguration": { // ここから丸っと追加。取得先の情報を設定。 "downloadMethod": { "introspection": { "endpointURL": "https://swapi-graphql.netlify.app/.netlify/functions/index", "includeDeprecatedInputValues": false, "httpMethod": { "POST": {} }, "outputFormat": "JSON" } }, "outputPath": "./schema.json" }
この状態で下記コマンドを実行すると、schema.json
が取得できます
$ bin/apollo-ios-cli fetch-schema
Queryを定義したgraphqlファイルの追加&設定変更
タイトルのリストを返却する簡単なQueryを AllTitles.graphql
に定義します。
このファイルはどこにおいても大丈夫なのですが、Query
というフォルダを作成しそこに格納しました。
query AllTitles { allFilms { films { title } } }
続いて、設定(apollo-codegen-config.json
)を変更します。
自動生成されるファイルは Generated
フォルダに格納されるようにします。
{ "schemaNamespace" : "SW", "input" : { "operationSearchPaths" : [ "**/*.graphql" ], "schemaSearchPaths" : [ "./schema.json" // ← 変更:今回はDLしてきたjsonファイルを指定 ] }, "output" : { "testMocks" : { "none" : { } }, "schemaTypes" : { "path" : "./API/Generated", // ← 変更:自動生成されるファイルの置き場 "moduleType" : { "embeddedInTarget" : { "name" : "API" } } }, "operations" : { "inSchemaModule" : { } } }, "schemaDownloadConfiguration": { ... } }
swiftファイルの生成
generateコマンドを実行することにより、swiftファイルが生成されます
ここまでで一旦下準備は完了です
タイトルのリストを返却するサンプル
ApolloClient
を直接利用しても良いのですが、簡易的にラップしたクラスを作成しました
import Apollo import Foundation public final class APIClient { private let apollo: ApolloClient public init(endpointURL: String) { // setup apollo client let cache = InMemoryNormalizedCache() let store = ApolloStore(cache: cache) let client = URLSessionClient() let provider = DefaultInterceptorProvider(client: client, store: store) let transport = RequestChainNetworkTransport( interceptorProvider: provider, endpointURL: URL(string: endpointURL)! ) apollo = ApolloClient(networkTransport: transport, store: store) } public func allTitles() async throws -> [String] { try await withCheckedThrowingContinuation({ continution in apollo.fetch(query: SW.AllTitlesQuery()) { result in switch result { case .success(let val): let titles = val.data?.allFilms?.films?.compactMap({ film in film?.title }) continution.resume(returning: titles ?? []) case .failure(let error): continution.resume(throwing: error) } } }) } }
これを用いて表示するViewのサンプルです。あらかじめAPIパッケージを利用できるようにXcodeの TARGETS > Frameworks, Libraries...
で設定しておきます。
import SwiftUI import API struct ContentView: View { private var client = APIClient(endpointURL: "https://swapi-graphql.netlify.app/.netlify/functions/index") @State private var titles: [String] = [] var body: some View { List(titles, id: \.self) { title in Text(title) } .onAppear(perform: { Task { titles = try await client.allTitles() } }) } }
実行結果
まとめ
プロジェクトに応じて設定ファイルをこねくり回す必要がありますが、一回作成してしまえば以降は必要なgraphqlファイルを追加するだけでほぼ作業が完結するので、開発体験は良かったです。
また、レスポンスに対応する型を手で書くと、Optionalの取り扱いなど慎重にならざるを得ないケースが多々ありますが、その問題も解決できるのが一番のメリットかなと思いました。
以上、何かの参考になれば幸いです。