このブログ記事は2015年6月に公開されたため、お読みいただいた時期によっては情報が古くなっている可能性があります。情報の正確性を保つため、これらの記事を常に最新の状態に保つことはできませんのでご了承ください。
iOS の Safari で最近起きたスクロールの問題を解決するために、自作のシンプルなテストページを作成し、インターネット上で見つけた情報や組み合わせをできるだけすべて試してみました。
このプロセスで私が発見したことの概要は以下のとおりです。
私のテストページ
私のテストページはかなりシンプルでした。ページを表す静的な div にモックデータを入れ、ページの上にオーバーレイとして配置される「メニュー」を表す固定の div を用意した、こんな感じでした。
このページの全ソースコードは、このブログ投稿の下部にあります。
デフォルトの挙動を理解する
最初にやったことは、iOS の Safari のデフォルト挙動を理解することでした。最も顕著な観察は、メニューを一番下までスクロールすると、ページ本文に「ラバー・バンド効果」が現れることです。これは Safari に組み込まれた仕様です。
メニューの一番上までスクロールしたときにも、同じことが起こりました。
ただし、最初に気づいた問題は、最上部または最下部までずっとスクロールし続けると、Safari が背景ページとメニューの両方にレンダリングの不具合を示し始めることです。
理由はよく分からないのですが、ゴムバンド機能に関係している気がします。
CSSルール「-webkit-overflow-scrolling」が何をするのかを理解する
次に行ったことは、メニューに -webkit-overflow-scrolling の CSS ルールを適用したことです。これにより モメンタムベースのスクロールを有効にします。
以下の画像に示されているとおり、指でスワイプしてもスクロールは続きます。
ただし、最上部または最下部までスクロールすると、下図のとおりラバーバンド効果が発生します。
ラバーバンド効果が完了するまでお待ちください。
テスト中にも、ラバーバンド効果が進行している間は、アニメーションが完全に完了するまで、ユーザーは別の要素へフォーカスを移すことができません。
下の例では、まず背景ページをスクロールし、それから素早くメニューをスクロールしようとしますが、結局背景ページのスクロールを続けてしまいます。
ただし、エフェクトが終わるまで少し待てば、メニューのスクロールを始められます。
バックグラウンドページのスクロールを防ぐ方法
テスト中にも、ラバーバンド効果が進行している間は、アニメーションが完全に終了するまで、ユーザーは別の要素にフォーカスを移すことができないことに気づきました。
この Stack Overflow の回答を基に、Stack Overflow の情報を活用すると、disable-scrolling を持つ要素が touchmove イベントをトリガーした際に、デフォルトのスクロール動作を行わないようにできます。
document.ontouchmove = function(event) {
var isTouchMoveAllowed = true,
target = event.target;
while (target !== null) {
if (target.classList && target.classList.contains('disable-scrolling')) {
isTouchMoveAllowed = false;
break;
}
target = target.parentNode;
}
if (!isTouchMoveAllowed) {
event.preventDefault();
}
};
続いて、ページの div 要素に disable-scrolling クラスを適用します:
<div class="page disable-scrolling">
この方法は一応動作しますが、より詳しい検証の結果、メニューの最上部または最下部に到達したときにバックグラウンドページが時々スクロールしてしまうことがあることが分かりました。
背景ページの過度なスクロールを防ぐ方法
この fix を適用することで、オーバースクロールを防ぐことが可能になります。これは、要素が最上部または最下部までスクロールされたとき、スクロールが本文へと続かなくなることを意味します。
その結果、ボディのスクロールが制限され、リバウンド効果の発生も防がれます。
function removeIOSRubberEffect(element) {
element.addEventListener("touchstart", function() {
var top = element.scrollTop,
totalScroll = element.scrollHeight,
currentScroll = top + element.offsetHeight;
if (top === 0) {
element.scrollTop = 1;
} else if (currentScroll === totalScroll) {
element.scrollTop = top - 1;
}
});
}
removeIOSRubberEffect(document.querySelector(".scrollable"));
この JavaScript のスニペットは、要素が最上部にも最下部にも到達しないよう、常にわずか1ピクセルだけ離しておくという動作をします。
スクロール位置が最上部または最下部に到達しなければ、オーバースクロールは発生しません。
バックグラウンドページへスクロールしすぎないようにする方法
メニューのリバウンド(ゴムのように伸びる)効果をなくしたい場合は、見た目のため、あるいは私の場合のように追加のレンダリング不具合を引き起こす場合は、-webkit-overflow-scrolling CSS ルールをただ削除してください。
ただし、以下のとおり、滑らかなスクロールの慣性も失われます。
ページ全体のソースコード
このページの全ソースコードです。
<!-- License MIT, Author Special Agent Squeaky (specialagentsqueaky.com) --> <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="minimum-scale=1.0, width=device-width, maximum-scale=1.0, user-scalable=no, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style> .page{ font-size: 24px; overflow: scroll; } .menu{ position: fixed; top: 0; bottom: 0; left: 0; width: 80%; background: gray; z-index: 1; font-size: 10px; overflow: scroll; /* uncomment to get smooth momentum scroll, but also a rubber band effect */ /*-webkit-overflow-scrolling: touch;*/ } .menu-item{ padding: 10px; background: darkgray; font-size: 24px; } </style>
</head>
<body>
<div class="menu scrollable">
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
<div class="menu-item">hello world</div>
</div>
<div class="page disable-scrolling">
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
when an unknown printer took a galley of type and scrambled it to make a type
specimen book. It has survived not only five centuries, but also the leap into
electronic typesetting, remaining essentially unchanged. It was popularised in
the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,
and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
</div>
<script>
document.ontouchmove = function(event) {
var isTouchMoveAllowed = true,
target = event.target;
while (target !== null) {
if (target.classList && target.classList.contains('disable-scrolling')) {
isTouchMoveAllowed = false;
break;
}
target = target.parentNode;
}
if (!isTouchMoveAllowed) {
event.preventDefault();
}
};
function removeIOSRubberEffect(element) {
element.addEventListener("touchstart", function() {
var top = element.scrollTop,
totalScroll = element.scrollHeight,
currentScroll = top + element.offsetHeight;
if (top === 0) {
element.scrollTop = 1;
} else if (currentScroll === totalScroll) {
element.scrollTop = top - 1;
}
});
}
removeIOSRubberEffect(document.querySelector(".scrollable"));
</script>
</body>
</html>