はじめに
この記事はand factory Advent Calendar 2020 の4日目の記事です。
昨日は@ykkdさんのSwiftlint autocorrectでコードを自動修正するでした!👏
AWS AppSync / Amplify
チャットアプリをはじめとしたモバイルのバックエンドとしてFirebaseが利用されることが多いと思いますが、
AWSのマネージドサービスには、他のAWSサービスとの連携が容易という大きなメリットがあります。
私自身iOSエンジニアではあるもののAWSの提供しているサービスや構成には興味があったので、
今回はAppSyncとAmplifyを使ってみることにしました。
AppSyncとはAWSが提供しているGraphQLでのサービス開発をサポートするマネージドサービスで、
GraphQLで用意されているサブスクリプション機能を使うことでチャット機能を比較的用意に作ることができます。
また、AmplifyはクライアントがAppSyncにアクセスするために提供されているツールで、
iOSではAmplify SDKを利用することで、AppSyncとのデータの受け渡しをSDK側が請け負ってくれます。
今回はAWS AppSync + Amplifyで爆速で(?)超ミニマムなチャットアプリを作ってみます。
Amplifyのセットアップ
前提条件
- AWSアカウントを持っている
- Amplifyのセットアップが完了している
- Amplifyを実行するコマンドラインツールのセットアップ、Podのインストールを行います。
- 詳細はこちらを参照ください。
Amplifyのコマンドラインツールで以下を実行します。
Amplifyの初期化
amplify init
その後プロジェクト名などの初設定を尋ねられるので回答します。今回は以下のように答えました。
? Enter a name for the project
-> SampleChatApp
? Enter a name for the environment
-> dev
? Choose your default editor:
-> Visual Studio Code
? Choose the type of app that you're building
-> ios
? Do you want to use an AWS profile?
-> Yes
? Please choose the profile you want to use
-> default
✅ Amplify setup completed successfully.
と出たら完了です。
APIのセットアップ
amplifyのセットアップが終わったら、コマンドラインツールを使ってAPIをセットアップしていきます。
プロジェクトルートで下記のコマンドを実行し、確認内容に沿って回答していきます。
amplify add api
再び質問されます。次はAPIについての初設定です。今回のケースの回答は以下です。
? Please select from one of the below mentioned services:
-> GraphQL
? Provide API name:
-> samplechatapp
? Choose the default authorization type for the API
-> API key
? Enter a description for the API key:
-> SampleChatApp's API key.
? After how many days from now the API key should expire (1-365):
-> 7
? Do you want to configure advanced settings for the GraphQL API
-> No, I am done.
? Do you have an annotated GraphQL schema?
-> No
? Choose a schema template:
-> Single object with fields (e.g., “Todo” with ID, name, description)
GraphQL schema compiled successfully.
? Do you want to edit the schema now?
-> Yes // Yesとするとエディタが開き、スキーマを編集できる
モデル定義
APIの作成の最後の質問にYesで答えるとエディタが開きスキーマを編集できるようになります。
GraphQLではgraphqlファイルで定義されたスキーマをもとにAPIを作成します。
ここでは簡単にメッセージのモデルを以下のように定義しました。
テキスト、作成日(エポックマイクロ秒を想定)、UserIDの最小限のプロパティです。
type Message @model {
id: ID!
text: String!
createdAt: String!
user: String!
}
スキーマを定義したら、
これまでに作成したスキーマやらAPIの定義ファイルやらのローカルのリソースをリモートにpushします。
amplify push
pushに成功すると、例によって質問がなされます。
この回答に基づいて作成したAPIにアクセスするためのラップ処理の種類や命名などが決まります。
? Do you want to generate code for your newly created GraphQL API
-> Yes
? Enter the file name pattern of graphql queries, mutations and subscriptions
-> graphql/**/*.graphql
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscripti
ons
-> Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested]
-> 2
? Enter the file name for the generated code
-> API.swift
ここまで行うとAmplifyのコマンドラインツールにてAPIが作成され、
AWSのコンソール->サービス→AppSyncから確認することができます。
クライアント実装
セットアップ
amplify init
の際に作成された、
amplifyconfiguration.json
とawsconfiguration.json
のふたつのjsonファイルを
Xcodeのプロジェクト内に移し替えます。
また、アプリ起動時のAmplifyのセットアップ処理としてAppDelegateで以下を実行します。
import UIKit
import Amplify
import AmplifyPlugins
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Setup Amplify
do {
try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels()))
try Amplify.configure()
} catch {
print("An error occurred setting up Amplify: \(error)")
}
// 略
return true
}
}
チャット機能実装
チャット機能の実装です。
viewDidLoadでデータソースに保存されたメッセージを取得する
REST APIでのGetはGraphQLではqueryが請け負います。
Amplify SDKではAmplify.API.query(request:)
を実行すして、Messageの配列を取得します。
また、件数を指定するlimitや次の値の参照を持つnextTokenを組み合わせることでページネーションを行うこともできます。
@IBOutlet private weak var tableView: UITableView!
var messages: [Message] = []
override func viewDidLoad() {
super.viewDidLoad()
self.fetchMessage()
}
func fetchMessage() {
// Amplify SDK経由でqueryオペレーションを実行しMessageの配列を取得
Amplify.API.query(request: .list(Message.self, where: nil)) { event in
switch event {
case .success(let result):
// GraphQLの場合、Query失敗時のerrorもレスポンスに含まれる
switch result {
case .success(let messages):
self.messages = messages
DispatchQueue.main.async {
// tableViewを更新
self.tableView.reloadData()
}
case .failure(let error):
// サーバーから返されるエラーはこっち
}
case .failure(let error):
// 通信エラー等の場合はこっち
}
}
}
送信ボタンでデータを投稿する
GraphQLにおいてデータの作成、変更などの書き込み操作はmutateが行います。
@IBAction func tappedSendButton() {
// キーボード閉じる
self.textField.resignFirstResponder()
// メッセージ内容
guard let text = self.textField.text, !text.isEmpty else {
return
}
// 送信時間を取得
let createdAt = String(Date().timeIntervalSince1970)
// 別管理しているUserID
let user = UserIdRepositoryProvider.provide().getUserId()
let message = Message(text: text, ts: ts, user: user!)
// mutateで新規メッセージを作成
Amplify.API.mutate(request: .create(message)) { event in
switch event {
case .success(let result):
switch result {
case .success(let message):
print("Successfully created the message: \(message)")
case .failure(let graphQLError):
// サーバーからのエラーの場合はこっち
print("Failed to create graphql \(graphQLError)")
}
case .failure(let apiError):
// 通信まわりなどのErrorになった場合はこっち
print("Failed to create a message", apiError)
}
}
// 初期化しておく
self.textField.text = ""
}
データソースの購読
最後にリアルタイムな結果の反映について実装します。
GraphQLではサブスクリプション機能を使うことによって、双方向のソケット通信を実現します。
Amplifyでは、Amplify.API.subscribe()
を実行することで、データソースの変更をレスポンシブに反映できるようになります。
override func viewDidLoad() {
super.viewDidLoad()
self.fetchMessage()
self.subscribeMessage()
}
func subscribeMessage() {
// 新たなメッセージの作成を購読する
subscription = Amplify.API.subscribe(request: .subscription(of: Message.self, type: .onCreate), valueListener: { (subscriptionEvent) in
// 購読したイベント内容をチェック
switch subscriptionEvent {
// サブスクリプションの接続状態の変更を検知
case .connection(let subscriptionConnectionState):
print("Subscription connect state is \(subscriptionConnectionState)")
// データの更新を検知
case .data(let result):
switch result {
case .success(let createdMessage):
self.messages.append(createdMessage)
DispatchQueue.main.async {
// テーブル更新
self.tableView.reloadData()
// 最新のメッセージまでスクロール
let indexPath = IndexPath(row: self.messages.count - 1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
case .failure(let error):
print("Got failed result with \(error.errorDescription)")
}
}
}) { result in
switch result {
case .success:
print("Subscription has been closed successfully")
case .failure(let apiError):
print("Subscription has terminated with \(apiError)")
}
}
}
ひとまず完成
できてるっぽい〜〜〜
やりきれなかったこと
できてるっぽい雰囲気が若干しますが、実は今回ソートができませんでした。
(サブスクリプションで購読したものは時系列順で取得できるのでごまかせるのですが、queryで取得したものはKeyのMessageのIDでソートされてしまい順序性が狂ってしまいます)
時系列順 | ID順(再度queryで取得した場合) |
---|---|
チャットであれば当然時系列順に並んでほしいところですが、超ミニマムということでお許しください。
(schema.graphqlをいじってソートキーにcreatedAtを指定することはできるのですが、
Amplify SDKで経由でどのように呼び出せばいいのかわからず。。知見がある人がいたら教えていただきたいです。)
おわりに
本記事では全く触れられませんでしたが、チャット機能に関しては
- 認証情報との紐付け
- 送信開始時点で送信中というステータスがユーザーに伝わるようにする
- 送信に失敗したときにユーザーに通知して再送信を促す
- 送信中にアプリを落としても送信が行われるようにする
- 画像や動画などコンテンツの拡充
- データの永続化をしてユーザービリティを高める
など考え出すとどんどん検討項目が出てくるので、
SDKにどこまでおまかせするべきなのか難しいところだなと思いました。
とはいえAmplify iOS SDKに関してはまだまだ調査段階なので、
また色々触って理解度深めていきたいところです。
最後まで見ていただきありがとうございました🙇♂️