Ionic7でもswipeToCloseを使いたいので代替実装してみよう
Ionic7では、swipeToCloseが廃止されました。
Re: swipeToClose being deprecated -- The current thinking is that modals should only be swipeable when either using the card modal or the sheet modal. The fact that non-card/sheet modals could be swipeable was a mistake on our end. This change is being done to better align with native iOS and Android behavior.
iOS、Androidのネイティブの整合性に合わせるために、カードモーダル、シートモーダル以外のモーダルはスワイプで閉じられないようになったとのことです。とはいえ、Twitterの画像ビュアー等、特定用途ではスワイプで閉じる方がユーザの利便性がよく、またユーザ自身も慣れているパターンもあるため、自分で swipeToClose の挙動を実装してみましょう。Angularでの実装です。
代替実装
Modalに用いてるページコンポーネント自身に実装します。基本方針としては、ページコンポーネント自体を ElementRef で取得し、その要素に対して touchstart と touchend を監視します。 touchstart と touchend が発火した時の touchmove の clientY を比較し、 touchstart が touchend より上にある場合に閉じるようにします。実装全体はこちらになります。
import { Component, ElementRef, OnInit } from '@angular/core';
import { IonicSlides, ModalController } from '@ionic/angular';
import { fromEvent, zipWith, withLatestFrom } from 'rxjs';
@Component({...})
export class ModalPage implements OnInit {
constructor(private modalCtrl: ModalController, private elementRef: ElementRef) {}
public ngOnInit() {
const watchSwipe$ = fromEvent<TouchEvent>(this.elementRef.nativeElement, 'touchstart')
.pipe(
zipWith(
fromEvent<TouchEvent>(this.elementRef.nativeElement, 'touchend').pipe(
withLatestFrom(fromEvent<TouchEvent>(this.elementRef.nativeElement, 'touchmove')),
),
),
)
.subscribe(([touchstart, [_, touchmove]]) => {
const touchstartClientY = touchstart.touches
? touchstart.touches[0].clientY
: touchstart.detail[1].clientY;
const touchmoveClientY = touchmove.touches ? touchmove.touches[0].clientY : touchmove.detail[1].clientY;
const yDiff = touchstartClientY - touchmoveClientY;
const threshold = touchmove.touches ? -50 : -5;
if (yDiff < threshold && touchstart.timeStamp <= touchmove.timeStamp) {
watchSwipe$.unsubscribe();
this.modalCtrl.dismiss();
}
});
}
}
fromEvent で、 touchstart と touchend を監視し、 zipWith で touchstart と touchend を結合します。 touchend と touchmove を結合するために withLatestFrom を使っています。 touchstart と touchend が結合された時点で、 touchstart と touchend の clientY を比較し、 touchstart が touchend より上にある場合に閉じるようにしています。 touchmove は、 touchstart と touchend の間に発火するため、 touchstart と touchend の timeStamp を比較し、 touchstart の方が後に発火している場合にモーダルを閉じるようにしています。
また、 touchmove.touches ? -50 : -5 で、マウスとタッチの挙動が異なるため、 touchmove がマウスの場合は clientY が大きくなるように、閾値を調整しています。もっと小さく、もしくは大きくしたい場合は、閾値を調整してください。またこれはModalすべてを対象としていますが、 ion-header だけを対象にしたい場合は、監視する対象を変更してください。
もっと簡単な代替
上記は「Modalの見え方をそのままに」という意図でつくっていますが、シート的な見え方になってしまってもよければ以下のように呼び出すことで、近い実装ができます。意図と用途にあった実装を選んでください。
this.modal = await this.modalCtrl.create({
backdropDismiss: true,
backdropBreakpoint: 0,
breakpoints: [0, 1],
initialBreakpoint: 1,
handle: false,
showBackdrop: true,
canDismiss: true,
component: MyModalComponent,
mode: 'ios'
});
まとめ
RxJSを使うと、 addEventListener での実装よりもシンプルに実装できていいですよね。それではまた。
Discussion