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