背景
個人アプリで使うスワイプ機能を調査していたところreact-tinder-card
というライブラリを見つけました。
こちらを使ってTinderみたいなスワイプ機能を作ってみましたので忘備録も兼ねて実装したコードの解説をしていきます。
完成デモ
実装したコード
余計な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;
実装で躓いたところ
サンプルコードから引っ張ってきたoutOfFrame
とswipe
という関数があるのですが
その中の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)
の処理が正常に走る理由は useImperativeHandle
がuseRef
にメソッドを付与するからだそうです。
※以下ライブラリから引用
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