SwiftUIとGraphQLを使って新規画面を作ってみた
こんにちは、publicationでは初投稿のかずおです。
はじめに
今回は新規画面開発で採用したSwiftUIとGraphQLの採用理由、実装内容、やってみた感想について紹介します。
GraphQLに関してはWebフロントとの組み合わせの記事は多く出てきます。
一方でネイティブアプリにGraphQLを採用した例はそれほど多くなく、参考にしてもらえると思い記事化しています。
新規画面について
弊社ではFANTSというオンラインでコミュニティを運営するためのサービスを開発しています。
今回はホーム画面という新規画面を作成しました。ホーム画面によってコミュニティの世界観やオーナーが今伝えたいことがユーザーに届きやすくなり、より良いコミュニティ運営ができることを期待しています。
SwiftUIとGraphQLの採用理由
SwiftUI
2022年の終わり頃にFANTSのiOSアプリの動作保証環境をiOS12.0以上からiOS14.0以上に引き上げました。
SwiftUIはiOS13から利用することができるので、このタイミングで使えるようになりました。
UIの構築や微調整はUIkitよりもSwiftUIの方が素早くできると考えていたので、新規画面を作るタイミングで採用に踏み切りました。
GraphQL
FANTSの既存画面は全てREST APIで作られています。ホーム画面はカルーセル画像、おすすめ投稿、イベント、コンテンツなどなど、さまざまな要素で構成されています。これらのデータをREST APIで取得する場合は以下の選択肢があります。
一つのAPIで全データを取得する
デメリット: APIの再利用性が下がる
APIを複数作成する
デメリット: エラーハンドリングが煩雑になる
REST APIの場合上記のようなトレードオフが発生しますが、GraphQLであれば取得するデータはクライアント側で選択できます。そのため、全データを一回のクエリで取得しつつ、再利用性の高い状態を保つことができます。
ホーム画面のような分散されたデータを取得、表示する画面と相性がよさそうだと思いGraphQLを採用しました。
実装内容
構成
バックエンド
FANTSのバックエンドはRailsで開発されています。今回はgraphql gemを利用し、RailsでGraphQLサーバーの構築をしました。
GraphQLの開発にはcode-firstとschema-firstの大きく分けて二つの開発手法がありますが、Railsのgraphql gemを採用する場合code-firstでの開発になります。バックエンドの実装とスキーマがずれないことはメリットですが、スキーマの確定はバックエンドの実装を待つことになります。
クライアント
GraphQLクライアントのライブラリはApolloを採用しました。
ApolloはWebのライブラリが一番有名ですが、モバイルの方も直近もメンテナンスされており、信頼性が高いと判断したため採用しました。
ただ、2023/7/11時点でApolloのバージョンはiOSはv1.xなのに対して、ReactとKotlinはv3.xでした。
Apolloの公式ドキュメントもiOS以外の方が説明が充実しているので、説明が足りないところは補完しながら見ると良かったです。
実装流れ
ざっくり以下のように進めていきました。
- UIの実装
- スキーマ共有
- query(fragment)の実装
- codeの自動生成
- UIへのデータの流し込み
UIの実装
今回モバイルの実装ではSwiftUIとGraphQLクライアントの導入でやることが盛りだくさんだったため特に気にしませんでしたが、code-firstの場合はバックエンドのGraphQLの実装を待つこともあるかと思います。
ただ、SwiftUIでの実装であればプレビューとmockデータさえあれば不自由なくUIの構築ができるので、データの流し込みの前までの作業はバックエンドの実装を待たずにできます。
スキーマ共有
code-firstのためバックエンドの実装を待ちます。(ある程度の共有はバックエンドの実装前にしていました。)
# 例
# *.graphqls
type User {
id: ID!
name: String!
email: String!
}
type Query {
users: [User]
}
query(fragment)の実装
スキーマが確定するとリクエストできるqueryや選択セットも決まっているので、(.graphql)という拡張子のファイルにqueryやfragmentを定義します。
# 例
# *.graphql
# Fragment for a User
fragment UserData on User {
id
name
email
}
# Query that uses the User fragment
query getUsers {
users {
...UserData
}
}
クライアントcodeの自動生成
apollo-iosの場合はこのページでコードの自動生成について見ることができます。
スキーマファイル(*.graphqls)と*.graphqlファイルからクライアントの言語にあった型とクエリ用の関数が自動生成されます。
# 例
# apollo-codegen-config.json
# 読み込むgraphqlファイルのパスを指定したりできます
"input": {
"schemaSearchPaths": [
"**/*.graphqls"
],
"operationSearchPaths": [
"**/*.graphql"
]
}
# コードの自動生成コマンド
./apollo-ios-cli generate
UIへのデータの流し込み
自動生成されたクエリ関数でデータを取得できます。(例: Apollo Docs Fetching Data)
<!-- .graphqlのコードが自動生成されたswiftコードでクエリすると↓になる -->
apollo.fetch(query: getUsers()) { result in
guard let data = try? result.get().data else { return }
print(data.users)
}
FANTSのiOSアプリではアーキテクチャにVIPERを採用しており、SwiftUIの実装もそれに則ることにしました。
SwiftUIのViewとpresenterのデータを@StateObject
でバインディングし、presenterのデータが書き換わったタイミングでUIの更新がされるように実装しました。
やってみた感想
SwiftUI
よかったところ
- UIの構築が容易
プレビューを見ながらUIの構築ができるので、スタイルの調整などもやりやすく素早くUIの構築ができました。また、リリース後にUIの調整があったのですがそれにも素早く対応できました。
わるかったところ
- UiKitでしか実装できない機能がある
今回諦めた機能として、Pull to Refreshでの画面更新があります。UiKitと組み合わせれば実現可能ですが、複雑な実装になると予想されたため工数と相談して今回の実装は見送りました。
Pull to RefreshはiOS15移行であればSwiftUIで対応しています。
GraphQL
よかったところ
- 一度に必要なだけのデータを取得できる
- APIの再利用性が高く、他画面に展開しやすい
- クライアント(iOS、Android、Web)とバックの型の共有がやりやすい
前評判通りの利点を感じることができました。特に弊社のWebの開発で採用しているスキーマ駆動開発のようなスキーマ・型の共有が、GraphQLの開発システムに組み込まれているのが魅力的だと感じています。
わるかったところ
- ネストが深いレスポンスを受けた時の処理が大変
採用しているアーキテクチャVIPERとの相性の問題かもしれませんが、データ層とView層を疎結合にするために、GraphQLで取得したデータからモデルへの詰め替えを行ない、View層へデータを伝搬していくようにしています。ここで、ネストが深いレスポンスだとデータの取り出しの際に型を当てるのが大変でした。
たとえばこんな感じ
# ネストが深いquery
query {
country(name: "Japan") {
name
city(name: "Tokyo") {
name
region(name: "Shibuya") {
name
district(name: "Harajuku") {
name
building(name: "Kawaii Building") {
name
room(number: "101") {
number
occupant
}
}
}
}
}
}
}
import Apollo
import Foundation
let client = ApolloClient(url: URL(string: "http://localhost:4000/graphql")!)
client.fetch(query: GetNestedDataQuery()) { (result: Result<GraphQLResult<GetNestedDataQuery.Data>, Error>) in
switch result {
case .success(let graphQLResult):
// データの取り出し
if let countryData: GetNestedDataQuery.Data.Country = graphQLResult.data?.country,
let cityData: GetNestedDataQuery.Data.Country.City = countryData.city,
let regionData: GetNestedDataQuery.Data.Country.City.Region = cityData.region,
let districtData: GetNestedDataQuery.Data.Country.City.Region.District = regionData.district,
let buildingData: GetNestedDataQuery.Data.Country.City.Region.District.Building = districtData.building,
let roomData: GetNestedDataQuery.Data.Country.City.Region.District.Building.Room = buildingData.room {
// モデルへの詰め替え
let room: Room = Room(number: roomData.number, occupant: roomData.occupant)
let building: Building = Building(name: buildingData.name, room: room)
...
...
...
}
case .failure(let error):
print("Failure! Error: \(error)")
}
}
GraphQLを将来的に剥がすことになってもいいようにこのような構成にしていますが、GraphQLのポテンシャルを最大限発揮するにはデータを利用するComponentでFragmentを宣言するFragment Colocationが必要になるかと思います。
まとめ
SwiftUIとGraphQLで新規画面を作成したときのことをまとめました。
筆者はどちらかというとモバイルよりもWebの方の実装に慣れた人間なのですが、SwiftUIは直感的にUIの構築ができ、それに対してGraphQLで自由にデータをとってくるスタイルはスッと馴染むことができました。
ただ、まだまだSwiftUIとGraphQLのポテンシャルを引き出せていないなと感じています。
今後はWebフロントとGraphQLの組み合わせを個人的に使ってみて、モバイルの開発に還元できればいいなと思っています。
Discussion