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

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

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

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

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

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

1. １ページ目を上端までスクロールする。
![iPhone上スクロール-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430093/6ecef37c-eeae-9b72-304b-c4ca053dd505.png)

1. 上端近くなったら次のページを読み込む。
![iPhone上スクロール-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430093/92f1ba17-6c0c-3410-dc3a-861d5af53e2d.png)

1. 読み込んだデータをコンテンツの先頭に追加する。
**しかし、このままでは「49.hogehoge」まで見ていたページが急に「99. ...」になってしまう**
![iPhone上スクロール-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430093/0119f5eb-d41e-5c8e-6cfe-1fe43703f0ee.png)

1. 追加する前に見ていた場所までスクロール位置を戻す。
![iPhone上スクロール-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430093/0c8fd22e-aae0-9634-2ce9-5094e35a0d52.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

```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: [],
    };
  },
  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](https://codepen.io/kilvistyle/pen/poywemY) で試してみたところ動作も問題なさそうです。

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

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

https://codepen.io/kilvistyle/full/poywemY



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

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

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

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

```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();
        }
      });
    }
  },
});

```


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

https://codepen.io/kilvistyle/full/vYGZmKp

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

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

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

    // スクロールイベントハンドラに紐付け、スクロール中は連続して呼ばれる
    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.js`や`vue-infinite-loading`には依存しない部分なので、例えば以下のようなjQueryなJavascirptにも簡単に適用することができました。

https://codepen.io/kilvistyle/pen/GRZEGjx
→[デモはこちら](https://codepen.io/kilvistyle/full/GRZEGjx)

このcodepenは [同じように悩まれた方のサンプルコード](https://teratail.com/questions/229804) をforkして修正してみました。
今回の問題を調査するのに、このfork元コードには大変お世話になりましたmm