7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【React】react-tinder-cardを使ってTinderみたいなスワイプ機能を実装する

Posted at

背景

個人アプリで使うスワイプ機能を調査していたところreact-tinder-cardというライブラリを見つけました。
こちらを使ってTinderみたいなスワイプ機能を作ってみましたので忘備録も兼ねて実装したコードの解説をしていきます。

完成デモ

ezgif.com-gif-maker.gif

実装したコード

余計なuseStateや型定義を取り除けばコピペで動くはずです。
各関数の役割はコメントアウトしています。

import React, { FC, useState, useRef, useMemo } from "react";
import TinderCard from "react-tinder-card";
import { useHistory } from "react-router-dom";
import ThumbUp from "../assets/icon/thumb_up.svg";
import ThumbDown from "../assets/icon/thumb_down.svg";
import Undo from "../assets/icon/undo.svg";
import { ProductDoc } from "../@types/stripe";
import Loading from "../components/Loading";

type Props = {
  db: Array<ProductDoc>;
};

const TinderSwipe: FC<Props> = ({ db }) => {
  const history = useHistory();
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [lastDirection, setLastDirection] = useState<string>();
  const [currentIndex, setCurrentIndex] = useState<number>(db.length - 1);
  /**
   * レンダリングされても状態を保つ(記録する)
   */
  const currentIndexRef = useRef(currentIndex);
  /**
   * dbのlengthだけuseRefを生成する
   * TinderSwipeを通すことでswipeメソッドとrestoreCardメソッドを付与する(useImperativeHandle)
   */
  const childRefs = useMemo<any>(
    () =>
      Array(db.length)
        .fill(0)
        .map((i) => React.createRef()),
    [db.length]
  );
  /**
   * useRefを更新する(valは基本 1 or -1 になる)
   */
  const updateCurrentIndex = (val: number) => {
    setCurrentIndex(val);
    currentIndexRef.current = val;
    if (currentIndexRef.current === -1) {
      setIsLoading(true);
      setIsLoading(false);
      history.push("/diagnose/result");
    }
  };
  /**
   * goback可能かを監視する
   * DBが5の場合4の時はgobackできない(初期gobackを不可にする)
   */
  const canGoBack = currentIndex < db.length - 1;
  /**
   * スワイプ可能かを監視する
   * DBが5の場合4,3,2,1,0と減っていく
   */
  const canSwipe = currentIndex >= 0;

  const outOfFrame = (idx: number) => {
    currentIndexRef.current >= idx && childRefs[idx].current.restoreCard();
  };
  /**
   * 手動でのスワイプの処理(押下式のスワイプも最終的にはこの関数を叩いている)
   * currentIndexを-1する
   */
  const swiped = (direction: string, index: number) => {
    setLastDirection(direction);
    updateCurrentIndex(index - 1);
  };
  /**
   * ライブラリのonSwipeを叩く > ローカルで用意しているswipeを叩く
   */
  const swipe = async (direction: string) => {
    if (canSwipe && currentIndex < db.length) {
      await childRefs[currentIndex].current.swipe(direction);
    }
  };
  /**
   * gobackする
   * currentIndexを+1する
   */
  const goBack = async () => {
    if (!canGoBack) return;
    const newIndex = currentIndex + 1;
    updateCurrentIndex(newIndex);
    await childRefs[newIndex].current.restoreCard();
  };

  return (
    <>
      {isLoading && <Loading />}
      <div className="tinder-swipe">
        <div className="cardContainer">
          {db.map((character, index) => (
            <TinderCard
              ref={childRefs[index]}
              className="swipe"
              key={character.name}
              onSwipe={(dir) => swiped(dir, index)}
              onCardLeftScreen={() => outOfFrame(index)}
            >
              <div
                style={{ backgroundImage: "url(" + character.images[0] + ")" }}
                className="card"
              >
                <h3>{character.name}</h3>
              </div>
            </TinderCard>
          ))}
        </div>
        <div className="buttons">
          <button onClick={() => swipe("left")}>
            <img src={ThumbDown} alt="" />
          </button>
          <button onClick={() => goBack()}>
            <img src={Undo} alt="" />
          </button>
          <button onClick={() => swipe("right")}>
            <img src={ThumbUp} alt="" />
          </button>
        </div>
        {lastDirection && (
          <h3 key={lastDirection} className="infoText">
            You swiped {lastDirection}
          </h3>
        )}
      </div>
    </>
  );
};

export default TinderSwipe;

実装で躓いたところ

サンプルコードから引っ張ってきたoutOfFrameswipeという関数があるのですが
その中のrestoreCard()swipe(direction)というメソッドは同じファイル内に定義されていないのになぜ処理が走っているのかわかりませんでした。

const outOfFrame = (idx: number) => {
    currentIndexRef.current >= idx && childRefs[idx].current.restoreCard();
  };

const swipe = async (direction: string) => {
    if (canSwipe && currentIndex < db.length) {
      await childRefs[currentIndex].current.swipe(direction);
    }
  };

上記のコードでrestoreCard()swipe(direction)の処理が正常に走る理由は useImperativeHandleuseRefにメソッドを付与するからだそうです。

※以下ライブラリから引用

React.useImperativeHandle(ref, () => ({
    async swipe (dir = 'right') {
      if (onSwipe) onSwipe(dir)
      const power = 1000
      const disturbance = (Math.random() - 0.5) * 100
      if (dir === 'right') {
        await animateOut(element.current, { x: power, y: disturbance }, true)
      } else if (dir === 'left') {
        await animateOut(element.current, { x: -power, y: disturbance }, true)
      } else if (dir === 'up') {
        await animateOut(element.current, { x: disturbance, y: power }, true)
      } else if (dir === 'down') {
        await animateOut(element.current, { x: disturbance, y: -power }, true)
      }
      element.current.style.display = 'none'
      if (onCardLeftScreen) onCardLeftScreen(dir)
    },
    async restoreCard () {
      element.current.style.display = 'block'
      await animateBack(element.current)
    }
  }))

初見でこれは流石に難しい…ライブラリのコードを追ってuseImperativeHandlの存在を知り、ようやく理解しました。

さいごに

実装の過程の中で先輩社員から下記記事を紹介していただき、急いでプログレスバーを追加しました。フロント実装を進める際は下記を意識すべきだと思いました。是非ご覧ください。

今回はReactを使って実装しましたが、Vue.jsにも類似のライブラリがあるので普段Vue.jsを使い慣れている方はそちらを試してみるのも良いかもしれません。

便利なライブラリは簡単に使える分、使っている側にとってどういう処理で動いているかブラックボックスになりがちだと思うので疑問に思ったら都度コードを追っていく必要性を感じました。

tinder-swipe-cardを使った記事はなかなか散見しなかったのでこの記事がどなたかの参考になりましたら幸いです。

参考URL

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?