14
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

iOSのSafariで上方向の無限スクロールの問題を解決する

Posted at

はじめに

LINEやSlackのような 上方向への無限スクロール が必要なWebアプリを作っていて、なぜか iOSのSafariだけうまく動作しない 問題に悩まされたので、原因とどう解決したかメモしておきます。
Vue.jsを使っていますがVue.js以外でも使えるテクニックです。

同じ事象で悩んでいる方の参考になれば幸いです。

上方向の無限スクロールでやっていること

そもそも無限スクロールとは コンテンツの端の方までスクロールしたら次のページを読み込みコンテンツに追加する という処理を繰り返すことで実現します。
下方向への無限スクロールの場合はこれだけです。

これが、上方向の無限スクロールの場合、次のページを追加する位置がコンテンツの先頭になるため、追加する前のスクロール位置に戻す必要があります。

イメージで説明すると・・・

  1. 1ページ目を上端までスクロールする。
    iPhone上スクロール-1.png

  2. 上端近くなったら次のページを読み込む。
    iPhone上スクロール-2.png

  3. 読み込んだデータをコンテンツの先頭に追加する。
    しかし、このままでは「49.hogehoge」まで見ていたページが急に「99. ...」になってしまう
    iPhone上スクロール-3.png

  4. 追加する前に見ていた場所までスクロール位置を戻す。
    iPhone上スクロール-4.png

仕組みとしては以上です。

vue-infinite-loading を試してみる

上記の処理を自作してもよいのですが、フロントエンドをVue.jsで作成していたので、定番そうなvue-infinite-loadingを使ってみました。
https://peachscript.github.io/vue-infinite-loading/

下方向だけでなく、上方向への無限スクロールにも公式に対応しているようです。
https://peachscript.github.io/vue-infinite-loading/guide/top-dir-scroll.html

vue-infinite-loadingの上方向無限スクロールサンプル(iOSのSafariだとうまく動かない)
const api = 'https://hn.algolia.com/api/v1/search_by_date?tags=story';

new Vue({
  el: '#app',
  data() {
    return {
      page: 1,
      list: [],
    };
  },
  methods: {
    infiniteHandler($state) {
      axios.get(api, {
        params: {
          page: this.page,
        },
      }).then(({ data }) => {
        if (data.hits.length) {
          this.page += 1;
          this.list.unshift(...data.hits.reverse());
          $state.loaded();
        } else {
          $state.complete();
        }
      });
    },
  },
});

codepen で試してみたところ動作も問題なさそうです。

そう、iOSのSafari以外ならね。

もし、iPhoneやiPadをお持ちであれば、以下のデモを試してみてください。
勢いを付けて上端にスクロールすると 一気に何ページ分ものコンテンツを読み込んでしまい、元の位置に戻ってこれていません。

なぜiOSのSafariではうまくいかないのか

ChromeやFirefox、MacのSafariでは問題なく動作するのに、なぜiOSのSafariではうまくいかないのか。
調べてみると、どうやら iOSのSafariの慣性スクロール がクセモノで、

慣性スクロール中はスクロール位置を変更する処理(*)が効かない ようです。
*scrollTop()scrollIntoView()など

そのため、上述した「4. 追加する前に見ていた場所までスクロール位置を戻す。」が動作せず、次ページ読み込み後も、スクロール位置がページ上端のままなので何ページもロードしてしまうという問題が起こっているようです。

解決方法

慣性スクロール中はJavascriptからのスクロール位置の変更は受け付けないので、慣性スクロールが停止するのを待ってから位置を変更することにしました。

vue-infinite-loadingの上方向無限スクロールサンプル(iOSのSafari対応版)

const api = 'https://hn.algolia.com/api/v1/search_by_date?tags=story';

new Vue({
  el: '#app',
  data() {
    return {
      page: 1,
      list: [],
      scrollEndTid: null,
      handleState: null
    };
  },
  mounted () {
    window.addEventListener('scroll', this.onScroll)
  },
  destroyed () {
    window.removeEventListener('scroll', this.onScroll)
  },
  methods: {
    onScroll() {
      clearTimeout(this.scrollEndTid);
      this.scrollEndTid = setTimeout(() => {
        if (this.handleState) {
          this.loadNews(this.handleState);
          this.handleState = null;
        }
      }, 200);
    },
    infiniteHandler($state) {
      this.handleState = $state;
      this.onScroll();
    },
    loadNews($state) {
      axios.get(api, {
        params: {
          page: this.page,
        },
      }).then(({ data }) => {
        if (data.hits.length) {
          this.page += 1;
          this.list.unshift(...data.hits.reverse());
          $state.loaded();
        } else {
          $state.complete();
        }
      });
    }
  },
});

修正版のデモは以下です。

iPhoneやiPadでの問題も解消されていると思います。
この修正のポイントは以下の「慣性スクロールの停止」を判定する部分です。

慣性スクロールの停止を判定する

慣性スクロールの停止判定

    // スクロールイベントハンドラに紐付け、スクロール中は連続して呼ばれる
    onScroll() {
      // timerをクリア
      clearTimeout(this.scrollEndTid); // dataのメンバ変数
      // 0.2秒のtimerを設定
      this.scrollEndTid = setTimeout(() => {

          // 0.2秒間スクロールされていない場合(=慣性スクロール停止時)の処理を記述

      }, 200);
    }

スクロール中は上記の onScroll() が連続して呼び出されるため、 clearTimeout()setTimeout() を使って 0.2秒以上次のスクロールハンドラが呼ばれなかったら停止した と判断するロジックを追加しています。

そして、次のページを読み込むタイミングはページの上端に到達した時に infiniteHandler($state) が教えてくれるので、その両方を満たす時に次ページをロードするようにしました。

これでiOSのSafariも含めて上方向の無限スクロールに対応できました :clap_tone2:

【参考】PureなJavascriptやjQueryへの適用

このiOSのSafariの慣性スクロールへの対応は、Vue.jsvue-infinite-loadingには依存しない部分なので、例えば以下のようなjQueryなJavascirptにも簡単に適用することができました。

https://codepen.io/kilvistyle/pen/GRZEGjx
デモはこちら

このcodepenは 同じように悩まれた方のサンプルコード をforkして修正してみました。
今回の問題を調査するのに、このfork元コードには大変お世話になりましたmm

14
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?