Tinder UI (React-Typescript)

Try the demo

Aug-22-2021 22-36-58

Try the demo here!

Requirements

  • スマホのブラウザ環境で動作すること ✅
  • カード下の左にスキップボタン、右にいいね!ボタンを表示する ✅
  • スキップボタンをタップしたときはカードが左に流れるアニメーションが実行され、次のカードが表示される ✅
  • いいね!ボタンをタップしたときはカードが右に流れるアニメーションが実行され、次のカードが表示される ✅
  • すべてのカードを仕分けできたら empty 画面が表示される ✅
  • テストを書く ✅
  • スワイプでカードを仕分けできる ✅
  • カードの下部をタップすると詳細画面が表示される ✅
  • API サーバーと連携する ✅

コードを書く前の計画

① UI design

Tinder-UI

figma で見てみる

② 前回作成したTinder-UIからの変更点

  • 前回はブランチを変えずに全ての開発を行っていたので今回は機能毎にブランチを切りイシュードリブンな開発で進めていく。
  • React の考え方でもある単一責任+Reusableなコンポーネント作りを重視して開発を進めていく。
  • テスト開発(テストを書く->テストを成功させるためのコードを書く->リファクター)を取り入れる。
    • コンポーネントが正しくブラウザー上にレンダリングされているか(Unit Test)。
    • ボタンを押した時にカードが正しくスワイプされているかどうか(Integration Test)。
  • masterブランチにコードが push された際にGitHub Actionsを使用してテストからデプロイメントまでを自動化させる CICD の導入。ホスティングサービスにはFirebaseを採用。
  • 使用言語、ライブラリーの変更。詳しくは下記 ④ に記載。

③ コンポーネント選定

Tinedr-UI-Components

④ フレームワーク、使用言語、ライブラリー

難しかったこと

難しかったことは細かい点が4つほどありました。

① スワイプ機能をモバイルブラウザーでテストしたところうまく動作しなかった。

  • 速度をつけずにゆっくりスワイプすると、少々動くがまた元の位置に戻る。
  • スクリーンをプッシュしている状態+素早くほんの少しだけスワイプするとスワイプされてしまう。

swipe bug

原因:

ブラウザーではディフォルトでタッチアクション、つまりズームしたり、タップしたりする動作の読み取りがONとなっているため、ブラウザーの解釈で自動的に画面上を更新してしまっているため。 この場合は、横にスライドしている動作をスクロールと認識してしまっているんだと思われる。

解決方法:

CSSプロパティーのtouch-actionnoneにしてあげると、ブラウザーのタッチアクションの動作の読み取りをOFFにして、コード側にその裁量権が与えられるので、うまくいく。

react-springreact-use-gestureがどのように組み合わさり、スワイプ機能を実現しているのか?

参考にしたコードはreact-springのドキュメントにも記載してあるこちらの例です。

何が起こっていたのか:

react-springuseSprings()react-use-gestureuseGesture()という関数たちが何をしているのかということを理解しているとスッと入ってくると思いました。

useSprings()は指定した数全てのオブジェクトにスプリングの機能(アニメーション)を与えます。この場合は一枚一枚のカードというオブジェクトに。 返り値は二つあり、ここでいうpropsという中身はオブジェクトArrayで一つ一つのオブジェクト内にはto()で指定した、プロパティー(x,y,scaleなど)がspringValueというスプリングアニメーション専用のタイプに変換され返ってきます。このプロパティーを変化させることでアニメーションを実現化せています。その値を変化させる事ができるのが二つ目の返り値であるset()という関数になります。reactuseState()みたいなものです。

const to = i => ({ x: 0, y: i * -4, scale: 1, rot: -10 + Math.random() * 20, delay: i * 100 })
const [props, set] = useSprings(cards.length, i => ({ ...to(i), from: from(i) }))

useGesture()という関数はユーザーが画面操作時に特定の動作(ここではドラッグされた時)が行われた瞬間、瞬間に呼び出され、コールバックの引数にはその瞬間時のスピードや位置などの情報が入ったオブジェクトが入ります。

const bind = useGesture(({ args: [index], down, delta: [xDelta], distance, direction: [xDir], velocity }) => {
    ..省略..
         set(
      return { x, rot, scale, ..省略.. } }
    })
    ..省略..
})

useDrag()useGesture()の主な違いはuseDrag()はドラッグ動作だけに反応するが、useGesture()は複数の動作を一度に組み合わせて使う事ができる。useDrag() + usePinch()をカードコンポーネントに与えるなど。

image

さてもう一度何が起こっていたかという話に戻ると、

  1. ユーザーがスワイプ動作をする。
  2. そのスワイプ動作の瞬間の値がuseGesture()によって取得される。
  3. その値を使い、ユーザーが何をしているのか(クリックされている、左に動いているなど)という事を判断し、その状況によってuseSprings()で作り出したプロパティーの値をset()で変えていくことによって動き(カードがSwipeされる)を作り出している。

react-springの*interpolate()という関数をなぜ使用するのか?

*現在ではto()が推奨されている。

上記の例から抜粋させてもらいました。

style={{ transform: interpolate([x, y], (x, y) => `translate3d(${x}px,${y}px,0)`) }}

なぜこれでは動作しないのか?

style={{ transform: `translate3d(${x}px,${y}px,0)` }}

なぜ?:

結論から先に言うとinterpolate()はダイナミックなアニメーションをさせたい時に使う。

CSSアニメーションには大きく分けて二つのアニメーションが存在している。

  1. Staticアニメーション

    opacityを0から1にしたり、translateYで100px上に動かしたりするなど、ブラウザーが全て行う単純な動作。

  2. Dynamicアニメーション

    Dynamicアニメーションとは反対に複雑なアニメーションで主にJavascriptで操作してアニメーションを行う。なのでユーザーの動作(スワイプ動作など)に合わせて複雑なアニメーションを作り出すことが可能となる。

以上の理由から、複雑なアニメーション(ダイナミックなアニメーション)を行いたい時はinterpolate()を使う。

複雑なアニメーションの例

image

上記のinterpolateの理解でいいと思うのですが、もう一歩深く調べたのでシェアします。

interpolateという英語の意味は 補間(ほかん)するという意味です。

内挿(ないそう、英: interpolation)や補間(ほかん)とは、ある既知の数値データ列を基にして、そのデータ列の各区間の範囲内を埋める数値を求めること、またはそのような関数を与えること。 またその手法を内挿法(英: interpolation method)や補間法という。

要は一般的には、あるデーターを基に不特定な部分を予測する事です。

アニメーション中での使われ方としては、ある瞬間とある瞬間の動きを定めてその間の動きは自動的に計算されるという事らしいです。 要は滑らかにしているという事です。例としてわかりやすいのは、CSSの@keyframesです。

この記事では動画の補間ソフトフェアを使いフレームレートが低い動画を補間して滑らかにすることによって動画が自然になっている事がわかります。

以上のことを踏まえてからもう一度コードを見ると、理解が深まりませんか?

style={{ transform: interpolate([x, y], (x, y) => `translate3d(${x}px,${y}px,0)`) }}

④ Github Actionsのワークフローに時間がかかっていた。

image

原因:

dependenciesのインストールに大きな時間がかかっていた。

解決方法:

yarn --prefer-offlineを使いyarn cacheのディレクトリーにすでにダウンロードされているキャッシュをなるべく使い、もし既存のキャッシュに存在しない場合にだけサーバーからダウンロードするという形を取る事でワークフローのトータル時間が減少した。

image

以上が私が感じた難しかったこと学んだことになります。

どこか間違っている点があればご指摘ください 🙇‍♂️