FirebaseとReact Nativeでオンラインホワイトボードを作る


この記事は 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のサービスでこんなのがあったとは…灯台下暗し。