はじめに
この記事はGoodpatch Advent Calendar 2020の11日目の投稿です。
弊社グッドパッチでは現在、クライアントや自社サービス、新技術検証など、あらゆる場面でプロトタイピングが熱く取り組まれています。
プロトタイピングに関する私の見解は、今年10月に行われた Goodpatch Engineer Meetup vol.6 にて語りましたので、こちらのレポートも是非ご参考ください。
そしてここ最近、あるクライアントワークでプロトタイプ実装の機会が巡ってきたので、今回はそこでの取り組みをご紹介します。
プロトタイプとは、そして作ったもの
検証対象にはいくつかの機能要素が含まれていたのですが、その中でも私のプロトタイプに対する以下の考えから**「チャット機能」**に絞り実装することにしました。
- プロトタイピングで重視すべきは
- プロトタイプを実装する行為そのものでなく
- 必要最小限の実装により、いかに検証成否を素早く得られるか
- そのため、世に既にあるものは積極的に利用し、ないものを自分で作る
- 既存のチャットツールにはない「一風変わった」要素があった
- 既存のサービスやありものを代用し検証可能なものは、実装優先度低
- 「動く」プロトタイプを実装するメリットは、よりリアルな状況下で検証できること
- 静止絵を見せながらのインタビュー検証ではなく、実際の日常利用による反応が検証のカギだと考えた
- インタビューで十分な検証が可能なものは、実装優先度低
「チャット機能」といえば、Push通知も必要不可欠。リアルタイムチャットに加えてPush通知も揃えて、わずか数日後に検証を始めるには...と考えた末、今回はFirebaseを活用することにしました。
- Realtime Database: ユーザー情報およびチャットの管理、クライアントへの変更通知
- Cloud Functions + Cloud Messaging: データベースの変更を受けてのPush通知発火
このおかげで、チャット機能をゼロから実装した経験はありませんでしたが、2日ほどで検証に必要な機能実装を終えることができました。(後述「参考」に挙げる記事の助けも欠かせなかったです。)
実物そのままはお見せできませんが、だいたいこんなイメージです。
Realtime Database
Realtime Databaseを用いて、
- ユーザー情報やチャットのやりとりをアプリからデータベースに書き込み、
- 逆にアプリ側ではその変更通知をリアルタイムに受け取る
ことが可能になります。
API設計や実装が不要なので、アプリ開発者に閉じて実装が行えることから、プロトタイピングの高速実装と非常に相性が良いと感じました。
文字通りリアルタイムにデータベースの変更をキャッチできるので、再読み込み処理といった実装も不要です。
データ構造
Realtime Database上のデータ構造は、だいたい以下のように定義しました。
root
├users:
├user1: あるユーザー情報
├id: ユニークなユーザーID
├name: ユーザー名
├fcm_token: FCMトークン(簡略してユーザーとトークンは1:1紐付け)
└status: ユーザー状態を表す値
├user2:
...
└chats
├chat1: ある二者間やりとりの情報
├ids: やりとり二者のID
├first_user_id: ユーザーID
└second_user_id: ユーザーID
└messages: やりとり履歴
├msg0: メッセージ
├date: 送信日付
├sender_id: 送信者のユーザーID
└text: メッセージ文字列
├msg1:
├msg2:
...
iOS側の実装
データベースに対して、アプリから以下のようにアクセス、監視します。(例: /users
以下の初回フェッチと変更監視)
Database.database().reference().child("/users").observe(.value) { (snapshot) in
guard let usersData = snapshot.value as? [String : Any] else { return }
usersData.forEach { (key, value) in //Realtime Database 上での key-value
...
}
...
}
またデータベースに対して、アプリから以下の方法でデータを変更、追加します。
let databaseRef: DatabaseReference
//子要素のパスを指定して値を設定
databaseRef.child("/any_path").setValue(["some_key_1" : someValue1, "some_key_2" : someValue2])
//ランダムなキー文字列を伴う子要素を生成して、値を設定
databaseRef.childByAutoId().setValue(["some_key_1" : someValue1, "some_key_2" : someValue2])
//例: メッセージの送信
chatWithUserADatabaseRef.child("/messages").childByAutoId().setValue(["date" : currentDate, "sender_id" : selfID, "text" : messageText])
Cloud Functions + Cloud Messaging
メッセージングアプリには欠かせないPush通知の実装です。自分に新着メッセージがあることを、Push通知で受け取れるようにします。
今回はプロトタイプのため、割り切って次のような設計方針を取りました。
JavaScript に不慣れかつ、Cloud Functions のコンソール上でのデバッグ作業が手間取ったためです。
- データベースの root 直下に、Push通知に必要なすべての情報をアプリから書き込み
- Cloud Functions 側でこの変更通知を受けて通知を発火する
root
└latest_message: chats内で最後に送られたメッセージ(Push通知用)
├message_text: メッセージ文字列
├sender_user_name: 送信者名
└receiver_fcm_token: 受信者のFMCトークン
Cloud Functions (JavaScript)
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.fireNewMessagePushNotification = functions.database.ref('/latest_message')
.onWrite((change, context) => {
if (!change.after.exists()) {
return null;
}
const latest = change.after.val();
const token = latest.receiver_fcm_token;
const payload = {
notification: {
title: latest.sender_name + "さんからのメッセージ",
body: latest.message,
badge: "1",
sound: "default"
}
};
const options = { priority: "high" };
return admin.messaging().sendToDevice(token, payload, options)
.then(pushResponse => {
//...
})
.catch(error => {
//...
});
});
}
以上です。Firebase様々でした。プロトタイプに欠かせない、強力なツールであることを実感しました。
また、肝心なプロトタイプの結果ですが、たった1週間の検証期間でも、現時点でも良い点、改善が必要な点が洗い出せ、中には会話の中では非常に期待が持てたアイデアも、検証を始めた途端大きな課題が発見されるなど、やはり机上のアイデアは形にして初めて価値に変わることを再認識しました。
おまけ
一般的な自分/相手の吹き出し表示仕分けですが、UIStackView
を用いて簡単に行うことができます。
ポイントは以下。
-
UIStackView
で構成する - Stack View の左右両端に Content Hugging Priority の低いスペーサーを配置する(
leftSpacerView
,rightSpacerView
)- メッセージバブルが文字量に応じて伸縮するようにするため
- 自分/相手の表示に応じて、必要/不要な表示要素は
isHidden
で切り替える
興味のある方は実装例もご参考ください。
参考
これらの記事なくして、無知識の状態からの短期実現はできなかったでしょう。
Firebase を使って30分でiOSのチャットアプリを作ってみる(新SDK対応版)
Cloud Function for Firebaseでチャットアプリのプッシュ通知を打つ
Cloud Functions for Firebaseとは?