🕌

Ionic7でもswipeToCloseを使いたいので代替実装してみよう

2023/04/18に公開

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.

https://github.com/ionic-team/ionic-framework/issues/25362#issuecomment-1139804151

iOS、Androidのネイティブの整合性に合わせるために、カードモーダル、シートモーダル以外のモーダルはスワイプで閉じられないようになったとのことです。とはいえ、Twitterの画像ビュアー等、特定用途ではスワイプで閉じる方がユーザの利便性がよく、またユーザ自身も慣れているパターンもあるため、自分で swipeToClose の挙動を実装してみましょう。Angularでの実装です。

代替実装

Modalに用いてるページコンポーネント自身に実装します。基本方針としては、ページコンポーネント自体を ElementRef で取得し、その要素に対して touchstarttouchend を監視します。 touchstarttouchend が発火した時の touchmoveclientY を比較し、 touchstarttouchend より上にある場合に閉じるようにします。実装全体はこちらになります。

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 で、 touchstarttouchend を監視し、 zipWithtouchstarttouchend を結合します。 touchendtouchmove を結合するために withLatestFrom を使っています。 touchstarttouchend が結合された時点で、 touchstarttouchendclientY を比較し、 touchstarttouchend より上にある場合に閉じるようにしています。 touchmove は、 touchstarttouchend の間に発火するため、 touchstarttouchendtimeStamp を比較し、 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