iPhoneやiPadで position: fixed; で絶対配置したエリア内をスクロールするとき、最上部をより上にスクロールしようとしたり、最下部をより下にスクロールしようとすると、ビヨーンとバウンスされずにロックされ、数秒固まる。このフリーズ状態はもはやiOSのバグとしか言いようがない。
はじめて気がついたのが2010年代半ばで、かれこれ数年の月日が流れている。さすがに、iOS側で対応してると思いきや未だ解決されていないことに気がついた...。そこで改めて最適な解決方法が生まれているのではとググる。同様に指摘された記事は見つかるが、残念ながら真新しいものはないどころか、それらの方法では解決できない...。
現在、自作したJavaScriptのいろいろなTIPSを整理しつつ、必要なTIPSはES6に書き直している。その中の一つが、本記事についての自作スクリプト。ということで、実例サンプルと合わせてES6に書き直した回避方法を共有させていただきます。
まずは、iOS Safariの実機でご覧ください。
何も処理していないページ(何が問題かを実際に確認できる)。
フリーズするバージョン
この問題をjQueryを使って解決したページ。
フリーズ回避 jQuery編
この問題を何のフレームワークやライブラリも使わず、JavaScript(DOM)を使って解決したページ。
フリーズ回避 JavaScript(DOM)編
ちなみに「position: fixed; とoverflow: auto; は同じ要素(タグ)に指定しない」や「-webkit-overflow-scrolling の値をtouchではなく auto にする」 と書かれた記事が多数あるが、これらでは解決しない。
「position: fixed; と overflow: auto; を同じ要素(タグ)に指定するとスクロールしない」という記事を見かけたが、これをご覧いただければ分かるとおり、2021年現在、position: fixed; overflow: auto; を同じ要素(タグ)に指定してもスクロールする。
「-webkit-overflow-scrolling: auto; にすると回避する場合がある」という記事も見かけた。惰性スクロールを無効にすることはUX視点では概念モデルと異なるので根本解決ではない。そもそも、2021年現在、-webkit-overflow-scrolling の指定自体が不要と判断している。実際、プログレッシブ・エンハンスメントな思考なので、2019年後半頃から指定するのはやめた。
そもそも、なぜ「-webkit-overflow-scrolling: touch;」を指定するようになったのか。
それをお話しするために、時をAndroid端末が市場に出始めた頃に戻そう。当時のAndroidのUXはそれはお世辞にも良いとは言えず、その代表格が画面スクロール時のカクカク。それから次々とアップデート版でUXが改善されていく中で「-webkit-overflow-scrolling: touch;」を使うとカクカクしなくなる、という情報が出た。また、iOS Safari でも、ページ内スクロールを実装しようとしたときのみ、そのエリア内のスクロール時はカクカクする現象があり「-webkit-overflow-scrolling: touch;」を使うことで回避できた。
だがしかし、それはもはや過去の話し。今どきの環境であれば、そもそも何の設定をしなくても、カクカク動くことはない。
と話しがズレました。本題に戻ります。
どのような条件でフリーズするのか。フリーズするには2つのケースがある。
注目したいのは、どのタイミングでフリーズするのか。
実機でいろいろ試した結果、ビヨーンと惰性している間ではなく、「ビヨーンが終わった後、スクロールが止まった時点」であることが分かった。
フリーズする条件が明確になったことで、逆にフリーズしない条件がわかる。
それでは、どのような条件ならフリーズしないのか、言語化してみる。
「ページ内スクロールが止まった時点」で「トップや最下部から1pxでも移動していれば」、フリーズしない。
というわけで、実装イメージが出来上がる。
このように、最上部や最下部の状態が発生したら、上部 + 1px、下部 - 1px、と調整することでフリーズしなくなる。
注意1)4. のスクロール位置が下部の場合は、1px上に移動するということ。つまり1px移動できる伸び代を用意する必要がある
注意2)スクロール領域より、その中のコンテンツ領域が短い場合に配慮をする(その場合でもビヨーンという惰性スクロールは発生させる)
補足)スクロール位置を判別して処理する間にスクロールときだけフリーズしてしまうことになるが、通常利用でフリーズすることは、限りなくない。
というわけで、これらの条件を満たすようにコーディングすれば完成だ。
ソースコードはGitHubで公開中。
GitHub: https://github.com/smplsmplsdsn/ios-safari-bugix-smooth-scroll
HTMLとCSSのポイントは2つ。
HTML
<div class="scroll">
<div class="scroll-inner">
(スクロールするエリア。ここに好きなだけマークアップする)
</div>
</div>
CSS
スクロール領域(.scroll)の幅と高さ(width, height)や固定エリアの表示位置(top, left)の値は適当。デザインに合わせて調整してください。ちなみに、スクロール領域の単位がpxでない場合、スクロール領域内の高さは、calcを使って指定する。
例:height: calc(100% + 1px);
.scroll {
position: fixed;
top: 20px;
left: 20px;
z-index: 1;
width: calc(100% - 40px);
height: 400px;
overflow: auto;
width: 100%;
height: 400px;
}
.scroll-inner {
min-height: 401px; // 親要素の高さ + 1px
}
最後にJavaScript。JavaScriptは、jQuery版とDOM版の2通りを紹介するので、どちらかお好きな方で。
個人的には、圧倒的にjQuery版をオススメする(不要な判別処理の必要がなく、コードをシンプルに保持でき、その箇所においての各ブラウザや各デバイスの検証を省けるため)。
ただし、フレームワークに依存してJavaScriptを記述している場合は、DOM版をベースに、そのフレームワークの記述ルールに従って書き直すことで、メンテナブルに実装できます(その場合は、GitHubにフレームワーク名をファイル名にした新規JSファルをプルリクもらえると、めっちゃ嬉しいです。GitHubの該当リンクはこちら)。
/**
* 【Bugfix】iOS Safari で惰性スクロールがロックされてしまうのを回避する
* https://www.simplesimplesdesign.com/web/markup/javascript/ios-safari-bugfix-smooth-scroll/
*
* 引数には、スクロール領域(position:fixed; overflow:auto; を指定している要素)を指定する
* display:none; の状態で実行しても何も動作しないので注意
*
* @param {object} tgt* jQueryでの要素指定 例) $(".js-ios-scroll")
*/
const bugfixScroll = (tgt) => {
let is_top = true,
is_bottom = false,
moving;
/**
* スクロール位置が上部、もしくは下部にあるとき1px移動する
*/
const checkScroll = () => {
let t = tgt.scrollTop(),
h = $("> :first-child", tgt).outerHeight(true) - tgt.height();
/**
* 0.01秒最上部より上の位置にある場合、1px下に移動し、
* 0.01秒最下部より下の位置にある場合、1px上に移動する
*/
const setPos = (v) => {
if (moving) clearTimeout(moving);
moving = setTimeout(function(){
tgt.scrollTop(v);
if (v === 1) {
is_top = false;
} else {
is_bottom = false;
}
}, 10);
}
// 小数点は切り上げて、整数にする
h = Math.ceil(h);
// スクロール位置が惰性で最上部より上の位置にあるか判別する
if (t < 0) {
is_top = true;
} else if (is_top){
setPos(1);
}
// スクロール位置が惰性で最下部より下の位置にあるか判別する
if (t > h) {
is_bottom = true;
} else if (is_bottom) {
setPos(t - 1);
}
}
// ページ上部にあるときは、1px下に移動する
if (tgt.scrollTop() == 0) {
tgt.scrollTop(1);
}
// tgt内をスクロールしている間、処理する
tgt.on("scroll", checkScroll);
}
$(function () {
bugfixScroll($(".scroll"));
});
/**
* 【Bugfix】iOS Safari で惰性スクロールがロックされてしまうのを回避する
* https://www.simplesimplesdesign.com/web/markup/javascript/ios-safari-bugfix-smooth-scroll/
*
* 引数には、スクロール領域(position:fixed; overflow:auto; を指定している要素)を指定する
* display:none; の状態で実行しても何も動作しないので注意
*
* @param {object} tgt* DOMで要素を特定指定 例) document.getElementsByClassName('js-ios-scroll')[0]
*/
const bugfixScroll = (tgt) => {
let is_top = true,
is_bottom = false,
moving;
/**
* スクロール位置が上部、もしくは下部にあるとき1px移動する
*/
const checkScroll = () => {
let t = tgt.scrollTop,
h = tgt.children[0].offsetHeight - tgt.offsetHeight;
// MEMO: h の取得だが、本来は、margin上下とpadding上下とborder上下の値をケアする必要があるが、ここでは割愛
// ただし、CSS側で、下記の2つのルールを採用すれば、このサンプルのように、JSでのケアの必要はない
// 1. tgt(.scroll)には、padding上下とborder上下を指定しない
// 2. tgt.children[0](.scroll-inner)には、margin上下を指定しない
/**
* 0.01秒最上部より上の位置にある場合、1px下に移動し、
* 0.01秒最下部より下の位置にある場合、1px上に移動する
*/
const setPos = (v) => {
if (moving) clearTimeout(moving);
moving = setTimeout(function(){
tgt.scrollTop = v;
if (v === 1) {
is_top = false;
} else {
is_bottom = false;
}
}, 10);
}
// 小数点は切り上げて、整数にする
h = Math.ceil(h);
// スクロール位置が惰性で最上部より上の位置にあるか判別する
if (t < 0) {
is_top = true;
} else if (is_top){
setPos(1);
}
// スクロール位置が惰性で最下部より下の位置にあるか判別する
if (t > h) {
is_bottom = true;
} else if (is_bottom) {
setPos(t - 1);
}
}
// ページ上部にあるときは、1px下に移動する
if (tgt.scrollTop === 0) {
tgt.scrollTop = 1;
}
// tgt内をスクロールしている間、処理する
tgt.addEventListener("scroll", checkScroll);
}
window.addEventListener('load', function () {
bugfixScroll(document.getElementsByClassName('scroll')[0]);
});
checkScrollの処理内で、都度高さを取得しているが、これはリサイズした場合に配慮している。
表示したときのみ高さを取得すれば良いように感じるかもしれないが、実際は、ラップトップであればブラウザサイズを変更したり、スマホであれば仮想キーボードを表示したり、処理中に高さが変更されることへの配慮だ。
また、なぜsetTimeout処理を使っているのか。これは、setTimeout処理をしないで、即時処理にしてしまうとビヨーンと惰性スクロールしなくなるためだ。そこで、0.01秒というタイムラグを作っている。この数値はあくまで個人的に実機でいろいろ検証した結果、妥当と判断した値だ。ちなみに、この間隔の時間が長いほど、最上部・最下部状態が長くなるため、フリーズする可能性が高まってしまう。
それでは最後に、iPhoneやiPadで確認できるページまとめ。
ソースコードはGitHubで公開中。
GitHub: https://github.com/smplsmplsdsn/ios-safari-bugix-smooth-scroll
fixed指定したエリア内をスクロールさせている事例。
Five Minute Diary
Webアプリ。タブで切り替えた際のコンテンツエリアで使用。
Sony Eマウント フルサイズ対応のレンズ収集
横幅が720pxより小さい場合の検索エリアで使用。
この書籍の中で、ライティングのユーザビリティについて触れている項目があり、激しく共感する部分だったので、ここで備忘録としてピックアップする。 ・・・
この書籍は、I部 リサーチ、II部 デザイン、III部 インプリメンテーションと三部構成になっている。この記事では、その中から、I部 リサーチ、・・・
これを知ったときはマジか!と飛び跳ねた。あらためて思い返してみると、確かに公式チュートリアルでも触れていたことをうっすら思い出す。そしてそのときとやり方がなん・・・
試したことはまだないけど、Macで編集時と書き出し後で色味が変わって困ったときのために備忘録。 プロジェクト設定 > 「カラーマネジメント・・・
プロジェクト設定と環境設定について詳しく解説されている情報がなかなか見つからないので、要所要所で知り得た情報を更新していきます。 プロジェクト設定 ・・・
Clipyが超絶便利そうだったので、macOS Big Sur にインストール&再起動して使ってみた。ところが、command + c を複数の箇所で行い、c・・・
requestAnimationFrame をはじめて見たので調べていたら、setTimeout や setInterval は requestAnimati・・・
6年ぶりに読み返したが、今なおハッとさせられることが多く見つかる。 この本で忘れないようにしたい項目をピックアップするだけでも役に立ちそ・・・
この記事では、以前に読んだ際に付箋を付けていたが、その部分だけを備忘録としてピックアップするため、本の要約ではない。実際は、何倍も濃い内容で、練・・・
もう一回、全部読み返したいところだが、書籍がありすぎるので、当時付箋紙をつけていたところだけをピックアップ。書籍内ではより具体的な説明があるので・・・
たとえば、白文字が混じった画像を透明GIFにする場合、背景だけでなく白文字までが透明化されてしまう。 ずっと悩んでいたがようやく解決できたので覚え書き。 形・・・
jQurey Mobile。まもなくβ版がリリースされるそうですが、現状のα版3を見てみました。 最低限必要なソースまで削り落として、ちょっとjQuery Mo・・・
変数bnにphpファイルのファイル名を拡張子なしで取得するサンプル $bn = basename($_SERVER['PHP_SELF'], ".php");・・・
sassファイルを保存すると、cssファイルが自動生成されるという症状でしばらくの間、ストレスだったがようやく解決。 例えば、var.scssを修正して保存す・・・
記述方法は間違いないのに、文字化けしてしまう。 xhtmlはutf-8で宣言していたのだが、Javascriptで記述された日本語がブラウザで確認すると文字化け・・・
変数bnにhtmlファイルのファイル名を拡張子なしで取得するサンプル var bn = location.href.split("/"); bn = bn[b・・・
ローカル環境はMAMPを使いました。 データベースを作成する MAMPを起動し、「サーバを起動」後、「オープンWebStartの」をクリックします。 「ツ・・・
xhtmlソース <div> <ul><!-- --><li><img alt="navi1" />・・・
Webサイトに動画を利用する際には、2017年8月時点では、webmとmp4とどちらにも対応していない場合の静止画像を用意して置くことにしている。 動画を違う拡・・・
正しいCSSを記述しているにもかかわらず、表示が崩れることがある。ブラウザによる解釈の違い(バグ)が原因であるが、IE5や古いブラウザにバグが見られる。・・・
「test」
「>ころころさん コメントありがとうございます! 確かに、APIドキュメント見ましたが、ライブに関する情報は今のところ取得できないようですね。 インスタライ・・・」
「SIGMAから超軽量で明るい単焦点レンズが2本発売になりましたので、リストにレンズ情報追加しました!」
「>名無しさん ご指摘、ありがとうございます! 記事冒頭の「まずはサンプル」のサンプルが動いていませんでしたので修正しました。」
「>前田さん コメントありがとうございます。 環境は分かりかねますが、修正したいファイルのパーミッションを604や644に変更すると書き込みできるようになります(・・・」
「アバターを取得する「get_avatar」についても追記しました!」
「「値をクリアする」を追加しました。チェックボックスやラジオボタンの値を何も選択していない状態に戻すときについて触れています。」
「>匿名さん コメントありがとうございます。 サンプルでは、beforeは何も処理していないので期待する表示にならない、で正解です。 クリックした後が、対応・・・」
「(匿名)さん コメントありがとうございます。何も設定変更していないのに、それまで使用できていたサーバーが突然使えなくなるのは辛いですよね、、。 ここで紹介したや・・・」
「コメントありがとうございます! そもそもできるかはわかりませんが、ソースを書き換える必要はあります。 下記記事などが参考になるかもしれません。 illustra・・・」
WEB制作マークアップJavaScriptiOS Safariで絶対配置(position:fixed)して惰性スクロールしてもロックしない、フリーズ回避方法 | シンプルシンプルデザイン