サーバーと同期してリアルタイムに更新する画面を実装する
先日行われたiOSDC Japan 2022。
クラスター社でも社員が登壇し、2つのテーマについて発表いたしました。
この記事では「サーバーと同期してリアルタイムに更新する画面を実装する」の内容を紹介します。
※iOSDC Japan:iOS関連技術をコアのテーマとした技術者のためのカンファレンス
スライド全体のリンク
今回のイベントの登壇者
前提①:clusterの構成について
前提として、まずはclusterの仕様について解説します。
clusterでは、仮想空間内の体験(inroom)はUnityで構築、仮想空間に入るまでのフレンドとのメッセージのやりとりやワールドの探索までの体験(outroom)は、各platformのネイティブ言語で構築しています。
具体的にはiOSならSwiftで書かれていて、Unity as a Libraryという仕組みを使っています。
本日はoutroom、つまりSwift部分で実際に使っている手法を実例として紹介します。
前提②:アプリの画面を更新する手法について
clientがAPIサーバーと通信して結果を表示するようなシンプルな画面を更新する手法としては大きく2つに分類できます。
1つはPull-To-Refreshや更新ボタンを配置してユーザーのアクションをトリガーに更新する方法です。
もう1つがサーバーの更新を検知してリアルタイムに更新処理をする方法です。
ユーザーの明示的なアクションについては実装もシンプルでイメージしやすいと思います。そこでこの記事では、タイトルにもある通り、後者の方法について紹介します。
前提③:リアルタイム更新が必要な場合とは
さて、実際にリアルタイム更新が必要となるのはどのようなユースケースなのか考えてみましょう。
具体的にはユーザーに情報をいち早く伝えたい場合に使うのが効果的です。
代表例としてふさわしいのはチャット機能ではないでしょうか。相手からメッセージが来たのに、更新が行われないようなチャット画面は体験としてよくありません。ですので、リアルタイム更新を採用するべきでしょう。
逆に無理にリアルタイム更新をするべきではないケースも考えられます。例えば、詳細画面の説明文。仮にアップデートがあったとしても、ユーザーが読んでいる最中に、突然更新がかかってしまうと、ユーザーの混乱を招きます。
このようなユーザーへの情報伝達の優先度が高くなく、逆に更新することによってユーザーの混乱を招くような場合には、リアルタイム更新を無理にする必要はないと言えます。
リアルタイム更新の実装例
リアルタイム更新で一番のポイントとなる更新通知の受信方法について紹介します。受信方法は様々な手法が考えられますが、今回は4つの手法を比較します。
メリット・デメリットを表にまとめました。
検討する際は、要件 / 仕様に併せて最適なものを選びましょう。例えば、既にFirebaseでDBを構築している場合はFirebaseを扱い、iOSにしか提供していないアプリの場合はSilent Pushを扱うと良いかとも思います。複数の手法をハイブリッドに使うことを考慮しても良いかもしれません。
clusterでの実例紹介
clusterでは、ダイレクトメッセージを受け取った時や、フレンドのリクエストを受け取った時にリアルタイム更新をしています。
clusterが採用している更新通知
clusterはWebSocketを採用しています。
clusterはモバイルアプリだけではなく、VRのスタンドアロンアプリなど様々なプラットフォームに対応していることから、Firebaseなどに依存した手法よりも柔軟性の高いWebSocketが良いという判断をしました。
また、DesktopやVRアプリの通知にもWebSocketを用いていることや、元々FirebaseのDBを利用していなかったことも理由の一つと言えるでしょう。
clusterでの設計事例
ここからclusterアプリでの実際の設計事例を簡単に紹介します。
clusterでは、コネクションの管理にRealTimeSyncManagerというclassを用いて、一元管理しています。また、ServerにあるAuthAPIを叩く事でWebSocket用の認証トークンが受け取れるようになっています。
そして、受け取ったtokenを用いて、WebSocketとの接続を行うようにしています。
接続後はWebSocketを通じてサーバーからはProtocol BufferでRevisionという値が送られます。
このRevisionはリアルタイム更新に対応している画面や機能単位で存在していて、初回接続時もしくはデータ更新時に送信されます。
これをRealTimeSyncManagerで受信し、RealTimeSyncManagerは外部に受信したrevisionのObservableを公開しています。
ちなみにclusterで使用しているのはRxSwiftですが、Combineでも同様の設計は可能だと思います。
clientアプリ内には機能ごとのRevisionを保持するためのclassが存在します。
ex:フレンド機能であればFriendSync class
これがRevisionの更新チェックを行い、更新されていればRepositoryにfetchのrequestを送るという流れです。
そこからはRepositoryはAPIをfetchしてViewModelに値を伝搬します。そして、それをbindしているViewに表示するという割と一般的なMVVMの構成で見られる設計になっています。
ポイントは、リアルタイムに同期する画面もそうでない画面も共通のアーキテクチャとなっていることです。(赤枠部分参照)
詳細にはRepositoryとViewModelの間にUseCaseが存在していたりもしますが、基本的な構造はどの画面も同様です
そのため、新たにリアルタイム更新を導入したい画面については、その画面に対応したSync classを追加するだけで、概ね簡単にリアルタイムの更新に対応することができます。
また、何らかの事情でリアルタイムの更新を辞めたくなった場合には、Sync classを削除するだけで、既存の画面実装には影響の無いように機能を剥がすことも可能です。
このような設計を用いて、clusterではリアルタイムの画面更新を実現しています。
UI更新時に考慮するポイント
UIをリアルタイムに更新する上でもっとも重要なポイントは、ユーザーの邪魔をしないことです。当たり前の事ではありますが、ここを考慮しないとユーザー体験が悪くなります。
例えばユーザーが何かを見ているときに突然インジケーターやエラーが表示されたり、また、画面スクロールをしている最中で勝手に戻ってしまったりしたら体験としてよろしくないですよね。
上記のことが起こらないように、Viewの更新時の処理やエラーハンドリングには気をつけましょう。
そのために、差分更新を効果的に使って、パフォーマンスが悪化しないようにすることも大事です。
UIKitの例になってしまいますが、最近ではDiffableDataSourcesのようなApple純正のAPIを用いることでも差分更新の実現が可能です。
DiffableDataSourcesはアニメーションなどもきちんとやってくれるのでどの部分が更新されたかなどもユーザーに伝わりやすくておすすめです。
まとめ
この記事では、
・リアルタイム更新が必要なユースケース
・更新通知の方法(4種類)
・clusterでの実例
・UIの更新方法
を紹介をしました。
リアルタイム更新を実装する機会は余り頻繁ではないかもしれませんが、もし実装する際は今日の話を参考にしていただければ幸いです。
===
クラスターではiOSエンジニアをはじめ各職種の採用を強化中です。
興味を持ってくださった方は、まずはカジュアルにお話ししましょう!