この記事は Firebase Advent Calendar 2020 の4日目の記事となります。
FirebaseとReact Nativeを用いて、リアルタイムで手書きを共有できるオンラインホワイトボードを作成したので、その設計や実装方法をご紹介します。
モチベーション
オンライン会議中にiPadの手書きをサクッと共有したかったことです。
類似のサービスはいくつかあったけど、閲覧者側にログインが必要だったり、機能が過多だったりしたので、もっとシンプルに共有できるアプリが欲しいと思いました。
要求仕様
- iPadの手書きをURLで共有できる
- 共有された側はブラウザ or アプリで手書きを閲覧できる
- 手書きの更新はリアルタイムで共有される
- 複数人で同時編集することができる
設計の検討
リアルタイムでデータを共有するためにFireStoreを使うのがよさそうです。
難しいのは双方向で編集するために、手書き情報をどのように保持するかです。
手書き情報を一枚の画像として保持すると、同時に複数の人が線を書いたときのデータの合体が困難になります。
そこで、線の一本づつをdocumentとして保持するようにしました。
そのためには線の情報を、FireStoreに保存できるデータとして表現する必要があります。そこで今回はsvgのpath情報として線の情報を保持することとしました。
svgを使うと線を以下のような数値にシリアライズして表現できます。
285,156.5 288,156.5 294.5,156.5 301,156.5 305,156.5 307.5,156.5 308.5,157.5
FireStoreの構成
一枚のホワイトボードをboard
ドキュメントで管理します。
board
の下に線(polyline
)のサブコレクションを持たせます。
アプリ側ではpolylines
コレクションをonSnapShot
で監視することで、リアルタイムのデータ更新を実現します。
チャットアプリがメッセージをやりとりする要領で、このアプリでは線(polyline
)をやり取りするイメージです。
React Native側の実装
React Nativeで手書きをどう実現するか考えたいと思います。
線の表示
React Nativeではsvg情報を描画するためにreact-native-svg
というコンポーネントが提供されているので、これを利用します。
背景の四角形と、線分を1本を描画するコードは以下になります。
import Svg, { Polyline, Rect } from "react-native-svg";
export const BoardScreen () => {
return (
<Svg height={height} width={width} viewBox={`0 0 ${width} ${height}`}>
{/* 背景 */}
<Rect
x={0}
y={0}
width={width}
height={height}
stroke="#000"
strokeWidth="1"
fill="#fff"
/>
{/* 線 */}
<Polyline
points="99.5,151.5 104.5,151.5 119,153.5 135,156 167.5,161 194.5,164.5 218,167.5 236,171 244.5,174.5 249,176 255.5,180 260.5,183.5 267,185.5 271,187.5 274.5,189 276.5,190 277.5,190.5 279,192 279.5,192 281.5,193 283,195.5"
fill="none"
stroke="#f00"
strokeWidth="3"
/>
</Svg>
);
}
タッチの検出
お馴染みのView
コンポーネントでタッチを検出できます。
export type Point = {
x: number;
y: number;
};
export const BoardScreen () => {
// 描画中の線分の点情報を配列で保持する
const [points, setPoints] = useState<Point[]>([])
const onTouchMove = (event: GestureResponderEvent) => {
const { locationX, locationY, touches } = event.nativeEvent;
if (touches.length === 1) {
// 描画中の線分の情報を追加する
setPoints([...points, { x: locationX, y: locationY }]);
}
}
const onTouchEnd = async (event: GestureResponderEvent) => {
if (points.length > 0) {
/* TODO: ここでFireStoreにpolylineドキュメントを追加する処理 */
// 描画中の線分の情報をクリア
setPoints([]);
}
}
};
return (
<View
onTouchMove={onTouchMove}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
onTouchCancel={onTouchCancel}
>
<Svg>
/* 略 */
</Svg>
</View>
);
}
実際には他にも細々したことをやっていますが、基本の部分は上記のようなコードで実現できます。
課題
線の描画の度にドキュメントを作成するので、通信量がそこそこ多くなります。
費用のことを考えるとFireStoreでなくてRealTime Databaseのほうが良いのかしれません。
FireStoreは書き込み/読み込みの度に課金されますが、RealTime Databaseはダウンロード/アップロードの容量ごとに課金されます。
今回の場合小さいデータを頻繁に読み書きするので、もしかするとRealTime Databaseのほうが費用的には安くなるのかもしれません。
ただデータの取扱いはFireStoreのほうがやりやすいし悩みどころです…
できたもの
いちおうストアで公開しています。
「air board - ホワイトボードをサクッとシェア」
https://apps.apple.com/jp/app/id1524182843
(余談)
先述のような「iPadの手書きをサクッと共有したい」というモチベーションで開発をしましたが、開発してからGoogleのJamBoardというサービスを知りました。
限定URLで簡単に手書きを共有できたりします。
正直JamBoardのほうがイケてます(笑)
Googleのサービスでこんなのがあったとは…灯台下暗し。