はじめに
LINEやSlackのような 上方向への無限スクロール
が必要なWebアプリを作っていて、なぜか iOSのSafariだけうまく動作しない 問題に悩まされたので、原因とどう解決したかメモしておきます。
Vue.js
を使っていますがVue.js
以外でも使えるテクニックです。
同じ事象で悩んでいる方の参考になれば幸いです。
上方向の無限スクロールでやっていること
そもそも無限スクロールとは コンテンツの端の方までスクロールしたら次のページを読み込みコンテンツに追加する という処理を繰り返すことで実現します。
下方向への無限スクロールの場合はこれだけです。
これが、上方向の無限スクロールの場合、次のページを追加する位置がコンテンツの先頭になるため、追加する前のスクロール位置に戻す必要があります。
イメージで説明すると・・・
仕組みとしては以上です。
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
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からのスクロール位置の変更は受け付けないので、慣性スクロールが停止するのを待ってから位置を変更することにしました。
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も含めて上方向の無限スクロールに対応できました
【参考】PureなJavascriptやjQueryへの適用
このiOSのSafariの慣性スクロールへの対応は、Vue.js
やvue-infinite-loading
には依存しない部分なので、例えば以下のようなjQueryなJavascirptにも簡単に適用することができました。
https://codepen.io/kilvistyle/pen/GRZEGjx
→デモはこちら
このcodepenは 同じように悩まれた方のサンプルコード をforkして修正してみました。
今回の問題を調査するのに、このfork元コードには大変お世話になりましたmm