はじめに
個人開発している睡眠に特化した育児記録アプリSleepのユーザーの方から日付の切替できるボタンが遠いとのご指摘をいただきました。
確かに日付を1日戻すことが多いと思いますが、左上に位置していて右利きの人にとっては一番遠い位置です。(スマホ100%を想定しています)
モバイルアプリの操作になれているからスワイプで日付変えたいと言われたので、挑戦してみることにしました。
ブラウザデフォルトの戻る機能がスワイプで発火する処理なので画面全体は難しいかもと聞き、日付の切替ボタンを下の方に置く方向で考えていましたが、下の方に置きたいボタンが多く、UIデザインが私には難しすぎて機能で解決した方が早いかもと思い、ダメでも何かしら学びがあるだろうと挑戦したら上手くいきました。
だれかの役に立つかも?と思い、記録したいと思います。
使用したライブラリ
使用したライブラリはSwiper.jsです。
イメージ的にはWeb制作の領域でカルーセルなどの実装に使用されそうなのですが、Swiperにはたくさんのイベントが用意されています。
その中から使えそうなものをいくつか試してみて、それっぽいのを探しました。
イベント
私がスワイプしたタイミングで処理を実行したかったので、
「touchStart」と「touchEnd」というイベントを組み合わせて左右なのでx座標の差がプラスなのかマイナスなのかで左右の判別できるなと思って、eventオブジェクトの中を確認していたら、「touchStart」と「touchEnd」で受け取れる座標ってほぼ同じで、「touchEnd」のオブジェクトに色んなプロパティがあり、その中に「touches」というプロパティがあり、さらにその中に「tartX,startsY,currentX,currentY,diff」があります。
swiper.touches.diff!!!!
きっとこれだとわかりますね。
touchStartのeventオブジェクトも確認して、diffで問題なさそうだとわかったので、使用するイベントは「touchEnd」だけで良いということがわかりました。
useSwiperフック
使うイベントもわかったことなので、フック作ります。(実際フックにしたのは最後ですけどね)
まずはコードから。
import { useCallback } from "react";
import { Swiper } from "swiper/types";
const MIN_SWIPE_DISTANCE = 30;
export const useSwiper = (
handleSwipeLeft: () => void,
handleSwipeRight: () => void
) => {
const handleTouchEnd = useCallback(
(event: Swiper) => {
const diff = event.touches.diff;
if (Math.abs(diff) < MIN_SWIPE_DISTANCE) return;
// スワイプ左右の確認
if (diff < 0) {
handleSwipeLeft();
} else {
handleSwipeRight();
}
},
[handleSwipeLeft, handleSwipeRight]
);
return { handleTouchEnd };
};
詳細
const MIN_SWIPE_DISTANCE = 30;
ここはスワイプしたと判断する最低限の座標のdiffです。
実際にスマホを使って操作してみて、これはクリックで指ちょっと移動しただけよ?みたいな範囲を私の主観で探って「30」という値に決めました。
無駄に反応した等の声が挙がればもう少し大きくしてみるつもりです。
こういった値は「マジックナンバー」と言うらしく、定数でなにを表した値なのかわかるように定義してからプログラム内で使用するようにした方が良いそうです。
レビュー様様です。
export const useSwiper = (
handleSwipeLeft: () => void,
handleSwipeRight: () => void
) => {
引数には、左右それぞれの処理を含む関数を受け取るようにしています。
if (Math.abs(diff) < MIN_SWIPE_DISTANCE) return;
ここで、dissの絶対値(左右によりマイナス、プラスどちらにもなり得るので絶対値です)がMIN_SWIPE_DISTANCE(30)未満ならreturnする処理を入れています。
あとは、左右に応じて処理を実行するだけです。
使用するコンポーネントの書き方
結論から。
"use client";
import { Swiper, SwiperSlide } from "swiper/react";
import { useSwiper } from "@/app/_hooks/useSwiper";
import { IsLoading } from "@/app/_components/isLoading";
import "swiper/css";
export default function Page() {
const { handleTouchEnd } = useSwiper();
if (isLoading) return <IsLoading />;
if (error)
return <div className="text-center">データの取得に失敗しました</div>;
return (
<Swiper className="" onTouchEnd={handleTouchEnd}>
<SwiperSlide className="min-h-dvh">
...中略...
</SwiperSlide>
</Swiper>
);
}
詳細
特筆すべきことほとんどないですが、
onTouchEnd={handleTouchEnd}
で「TouchEnd」イベントでhandleTouchEndの処理を行います。
最初、古いの見ていたのか、公式ではありましたが
const swiper = new Swiper('.swiper', {
// ...
});
swiper.on('slideChange', function () {
console.log('slide changed');
});
上記の記法を用いて上手くいかなかったです。
ポイント
SwiperコンポーネントとSwiperSlideコンポーネントでスワイプしたい範囲全体をラップすることです。
最初、Swiperコンポーネントだけでラップしていたのですが、なんかうまくいかず、、
要素見るとスワイプに反応してほしい範囲が、swiperクラスを持つ要素では囲まれていましたが、swiper_wrapperクラスは存在しているけど子要素には何も入っていない状態になっていました。
恐らく重要なのはswiper_wrapperクラスに囲まれることのようだったので、SwiperSlideコンポーネントが必須だというのがポイントかなと思いました!!
おわりに
画面スワップで処理を行いたいことってモバイルアプリ以外であるのか疑問ではありますが、やりたいという機会があればuseSwiperフック使っていただければできるのではないかと思います。
私はUIデザインに苦戦してスワイプ処理を実装するに至りましたが、リクエストくださったユーザー様は感動してくださったので本当に嬉しかったです!!
リクエストには前向きに出来る限りお答えして、より良いアプリにしていきたいと改めて思わせていただけるとても良い機会となりました!