Cloud FirestoreでiOSアプリのオンラインステータスを監視
Firebase Realtime Database との組み合わせ
Cloud Firestoreを使ってiOSアプリのオンラインステータス監視を実装していきます。
オンラインステータスとは、例えばSlackの緑マークのように、ユーザーがそのアプリを現在開いているかどうかを示すものです。
本記事の内容はこの公式ドキュメントと似ています。
公式ドキュメントは、Cloud FunctionsがJavaScriptで書かれていて、さらにクライアントもWebアプリですが、本記事ではCloud FunctionsをTypeScriptで記述してクライアントもiOSアプリをSwiftで書いていきます。また、より分かりやすい説明となっているはずです。
下準備
まず、下準備として以下を済ませている前提とします。
- ✅ Firebaseプロジェクトのセットアップ: Cloud Firestoreの勘所 パート2- 投稿型のブログサービスを設計しながらデータ構造について考える の後半
- ✅ Cloud Functionsのテスト環境構築: Cloud Firestoreの勘所 パート4- Cloud FunctionsのFirestoreトリガーの自動テスト🤖
オンラインステータス監視の処理概要
まず、オンラインステータスを監視する際に難しいのが、オフラインになったタイミングの検知とそれをクラウドに伝えることです。そもそも、オフラインになったことを検知した後ではクライアントからクラウドにそのことを伝える手段がありません(オンラインに復帰してから伝えても意味がありません)。
つまりオフラインになったタイミングで任意の処理を実行するには次の制御が必要です。
- クライアントアプリで、切断時にクラウドに伝える処理を予約しておく
- クライアントアプリから切断されたら、クラウド側でそれを検知して予約されていた処理を実行
Firestoreにはこれを簡単にできる仕組みが用意されていません。一方、Firebase Realtime Databaseにはこれを簡単に実現できるAPIが備わっています。
つまり、Firebase Realtime Databaseを併用することによって、Firestoreでのクライアントのオンラインステータス監視が可能となります。具体的には次のような流れとなります。
- クライアントでオンライン化・オフライン化のタイミングでFirebase Realtime Databaseのデータを更新するように設定
- Cloud Functionsで、Realtime Databaseトリガーを用いてデータをFirestoreにデータコピー
- クライアントでオンライン化・オフライン化のタイミングでFirestoreのデータが更新されるようになるので、クライアントはそれを監視することによって自身や他のクライアントのオンラインステータスをリアルタイムで受け取ることができる
図にすると、次のようになります。
出来上がると次のようになります(アプリを起動してオンラインになりいきなりクラッシュさせてオフラインに戻るデモです):
そもそも、Cloud FirestoreにコピーせずともFirebase Realtime Databaseを直接参照すれば良いのでは?
なぜFirebase Realtime Databaseを直接参照せずにCloud Firestoreにコピーしてそれを参照しているのか疑問に思うかもしれませんが、要件がそれで満たせればFirebase Realtime Databaseを直接参照でも良いです。同じサービス・アプリで要件によってFirestoreとRealtime Databaseを併用することは普通にあり得ます。
ただ、FirestoreにコピーするとFirestoreのユーザードキュメントにstatus情報が含まれることになり、例えばFirestoreの users
取得の1クエリだけでstatus情報付きのユーザー一覧が取れるなど、諸々扱いやすくなります。
クライアントでオンライン化・オフライン化のタイミングでFirebase Realtime Databaseのデータを更新
Firebase Realtime Databaseのセットアップ
Firebase WebコンソールのDatabaseにてRealtime Databaseに切り替えて確認します。初めは次のように空になっています。
RULESを確認すると、デフォルトでは次のように認証済みであればどのデータも読み書きできるようになっています。
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
上の図のイメージのように/status/{userId}
配下に自身の状態をセットできれば良いので、次のようにその書き込み権限のみ許可するように変更します。
{
"rules": {
".read": false,
"status": {
"$userId": {
".write": "$userId === auth.uid"
}
}
}
}
次のようにこのルールをデプロイすると反映されます。
firebase deploy --only database
iOSアプリのオンライン化・オフライン化のタイミングでデータを更新
iOSアプリでは、以下のコードを起動直後に呼ぶだけです。
自身の接続状態を監視して、接続が確立されたらstateをonlineに変更していますが、それとともにオフラインになったタイミングで更新したい処理(stateをonlineに変更する処理)を予約しているのがポイントです。こうやってRealtime DatabaseのAPIを使った簡単なコードを書くだけで以下の本来やや難しい課題が簡単にクリアできました。
まず、オンラインステータスを監視する際に難しいのが、オフラインになったタイミングの検知とそれをクラウドに伝えることです。そもそも、オフラインになったことを検知した後ではクライアントからクラウドにそのことを伝える手段がありません(オンラインに復帰してから伝えても意味がありません)。
Firebase Webコンソールで確認すると、アプリの状態に応じて、次のようにonlineとofflineが即座に切り替わることが確認できるはずです。
ちなみに、先ほどのコードは非同期処理でネストがすでに2段階できてしまっているので、実プロダクトではRxSwiftなど使ってネストが深くなる問題など解消するのをおすすめします。
Realtime DatabaseからFirestoreへデータをコピー
Cloud Firestoreの勘所 パート2- 投稿型のブログサービスを設計しながらデータ構造について考える の後半はFirestoreトリガーからのデータコピーをしましたが、今回はRealtime Databaseトリガーを元にFirestoreへデータをコピーします。大まかな処理としてはかなり似ています。
まず、 src
直下に databaseTriggers.ts
というファイルを追加して、次のような onStatusUpdated
という関数を定義します。Realtime Databaseの /status/{userId}
を監視して、その status
データをFirestoreの users/{userId}
にセット(コピー)しています。
if (newStatus.lastChanged > eventStatus.lastChanged) { return; }
としているのがポイントで、トリガーを受け取ったタイミングでRealtime Databaseのstatusがさらに更新されていたら処理をスキップして次のトリガーに任せるようになっています。
今回は別ファイルに定義したので、 index.ts
にてimportして、
import * as databaseTriggers from './databaseTriggers';
さらに次のように関数が外から見えるように export
します。 index.ts
に記述した外からアクセス可能な関数がCloud Functionsの関数として検出されるようになっています。
export const onDatabaseStatusUpdated = databaseTriggers.onStatusUpdated;
そして、デプロイします。
firebase deploy --only functions
すると、クライアントアプリの状態に応じてFirestoreでも次のようにデータが更新されるようになりました🎉
クライアントアプリで、そのユーザーのstateが”online”の時はオンラインを示す緑アイコンにしたり、あるいは”offline”でも lastChanged
からの経過時間に応じて表示を変えたりしても良いですね。
ここから先の、各ユーザーデータを参照・監視処理は一般的なFirestoreの使い方でできるので割愛します。ドキュメントとしては次の2つにとても分かりやすく書かれています。
Cloud Functionsのテスト
Cloud Firestoreの勘所 パート4- Cloud FunctionsのFirestoreトリガーの自動テスト🤖 にて説明したオンラインモードの自動テストを今回の処理にも書いて行きましょう。実際の開発ではデプロイ前に済ませておくのが望ましいですが、今後関数が壊れにくくするために後から追加で書くのもまあまあありだとは思います。
次のように書けます。
Firestore自身にオンライン・オフライン検知の仕組みが備わっていないとはいえ、基本的な実装コード自体はクライアント・クラウド合わせて50行ほどで書けました。こういう一昔前だとけっこうな手間やコストがかかったこともお手軽に実現できて、Firebaseは素晴らしいサービスだなあと改めて思いました🔥