JavaScript

iOS Safariで絶対配置(position:fixed)して惰性スクロールしてもロックしない、フリーズ回避方法

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; を同じ要素(タグ)に指定してもスクロールします

「position: fixed; と overflow: auto; を同じ要素(タグ)に指定するとスクロールしない」という記事を見かけたが、これをご覧いただければ分かるとおり、2021年現在、position: fixed; overflow: auto; を同じ要素(タグ)に指定してもスクロールする。

-webkit-overflow-scrolling は全く関係ありません

「-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でも移動していれば」、フリーズしない。

というわけで、実装イメージが出来上がる。

  1. ページ内スクロールを監視する
  2. ページ内スクロールが止まったとき、スクロール位置を取得する
  3. スクロール位置が最上部の場合は、1px下に移動する
  4. スクロール位置が最下部の場合は、1px上に移動する

このように、最上部や最下部の状態が発生したら、上部 + 1px、下部 - 1px、と調整することでフリーズしなくなる。

注意1)4. のスクロール位置が下部の場合は、1px上に移動するということ。つまり1px移動できる伸び代を用意する必要がある
注意2)スクロール領域より、その中のコンテンツ領域が短い場合に配慮をする(その場合でもビヨーンという惰性スクロールは発生させる)
補足)スクロール位置を判別して処理する間にスクロールときだけフリーズしてしまうことになるが、通常利用でフリーズすることは、限りなくない。

というわけで、これらの条件を満たすようにコーディングすれば完成だ。

サンプルソース

ソースコードはGitHubで公開中。
GitHub: https://github.com/smplsmplsdsn/ios-safari-bugix-smooth-scroll

HTMLとCSSのポイントは2つ。

  • スクロール領域に子要素を用意してその中に、コンテンツを入れる
  • スクロール領域の子要素には、高さにスクロール領域 +1px を指定する

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の該当リンクはこちら)。

JavaScript(jQueryを利用している場合)

/**
 * 【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"));
});

JavaScript(DOM)

/**
 * 【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]);
});

JavaScriptの余談

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より小さい場合の検索エリアで使用。

何かお困りではありませんか?

さいごまで記事を読んでいただき、ありがとうございます!
シンプルシンプルデザインでは、Web制作全般(企画から実装、運用まで)に関わらせていただくことができます。ホームページ制作・改修 / WordPress構築 / Webデザイン / UI・UXデザイン / Webディレクション / 広告運用 / ブランディング / マーケティング / 撮影・レタッチ / 動画撮影・編集 / プロトタイプ制作、幅広く対応しております。
何かお困りごとがあれば、何かしらのスキルがお役に立てるかもしれません。まずはお気軽にご相談いただけると嬉しいです!ご連絡お待ちしております。
Stay Safe.
» シンプルシンプルデザインのこと

最近の記事

記事の投稿IDなどの投稿者(ユーザー)情報を取得する方法 → get_the_author_meta

記事の投稿者(ユーザー)情報を取得するには、これまでは「get_userdata」を使っていたが、IDを取得したいだけなのに、もうちょっとシンプルな方法がある・・・

WordPress

「私たちの働き方とマネジメント」を読んで

数年前に読んだ本。本に付箋紙が大量についていたので、その箇所を読んで発見や共感をピックアップ。 最高のスマート・クリエイティブを惹きつ・・・

起業向け情報

「副業するならカメラマン」を読んで

2019年10月に書いたレビューの転載。 まず、タイトルや帯タイトルにあおられないでほしい。 この本は副業カメラマンになるための・・・

起業向け情報

「やりたいことを全部やってみる」を読んで

2019年9月に書いたレビューがあったので、ここに転載。--- タイトル「やりたいことを全部やってみる ストレスフリーな生き方を叶える・・・

起業向け情報

「世界一やさしいブログxYouTubeの教科書1年生」を読んで

2019年1月に書いたレビュー(2年以上前!)が残っていたので、ここに転載。当時YouTubeをやろうとしていた頃を思い出す。今でもYouTube更新しようと・・・

知識編

「図解モチベーション大百科」を読んで

書籍「図解モチベーション大百科」。主に行動経済学、実験心理学を扱った国内外の文献から、編著者なりの解釈で紹介された本。ここでは備忘録として書・・・

マーケティング・ブランディング

ローカル環境(MAMP)でgmailからメール送信できるようにする

MAMPにWordPressを入れてテストをしていると、ユーザー作成時やら何かとメール送信が必要になるタイミングがる。何も設定をしていないとローカル環境のWo・・・

MAMP

ユーザビリティと機能はどちらが上?

先日、「iOS Safariで絶対配置(position:fixed)して惰性スクロールしてもロックしない、フリーズ回避方法」という記事を書いたときに、エンジ・・・

デザイン思考

ビジネスカードとショップカード

以前にブランディングページを用意させてもらった、Sunny Side Accessory(サニーサイド アクセサリー)さんのビジネスカードとショップカードを制・・・

お仕事・制作日記

DtoC におけるプロモーションと広告運用

公開が2020年7月ということで、もう少しで1年経つのかぁ。「最終回 株式会社北の達人コーポレーション 木下勝寿社長 DMM MARKETING CAMP x・・・

マーケティング・ブランディング

人気の記事

WP_Queryを使って絞り込み検索するのにコレは便利!

めっちゃ参考になるページを見つけた。 絞り込みをするたびに色々ググっていたが、これがあればほとんどのことが解決できそうな気がする。 参考: これは便利!Word・・・

WordPress

iOS Safariで絶対配置(position:fixed)して惰性スクロールしてもロックしない、フリーズ回避方法

iPhoneやiPadで position: fixed; で絶対配置したエリア内をスクロールするとき、最上部をより上にスクロールしようとしたり、最下部をより・・・

JavaScript

Instagram API を使ってインスタに投稿した画像データを取得する

インスタに投稿した画像を自動でホームページでも更新されるようにしたい。もっとも簡単に実現する方法を紹介する。 公式リファレンスInstagram・・・

API

http://localhost:8888/ → http://localhost/

http://localhost:8888/ での参照ではなく、http://localhost/ で参照できるようにします。 MAMPの設定を変更する M・・・

MAMP

iframeの高さを自動調整する

iframeで呼び出した際、iframe自体はスクロールさせずに高さを取得してiframe全体を表示させます。 サンプル:iframeの高さを自動調整するデモ・・・

JavaScript

overflowをautoにした要素のスクロール位置を変更する → scrollTop scrollLeft

まずはサンプル。 これは、overflowをautoにした要素のスクロール位置を変更するサンプルです。 これは、overflowをautoにした要素のスクロ・・・

JavaScript

イラストレーターでレイヤーごとにPNGで書き出す

【2020年07月13日】 macOS Catalina(10.15.5) / Adobe iLLustrator 2020(24.2.1) にて動作確認済・・・

Illustorator

配列の重複を削除する

配列の場合 /** * 配列の重複を削除する * * @param {array} ary* * @return {array} 処理後のary・・・

JavaScript

WordPressのカテゴリー一覧ページでカテゴリー名とスラッグを取得する → get_queried_object()

やりたいこと カテゴリー一覧ページで、そのカテゴリー一覧ページに属する記事一覧を取得するためにループさせるのとは別に、タイトルやカテゴリー別にデザインを変・・・

WordPress

favicon.icoをWordPressの管理画面からアップロードできない→ wp-config.php を一時的に修正

いつもは直接アップロードしているので気にしていなかったが、WordPress管理画面のメニュー「メディア」からfavicon.icoをアップロードしようとした・・・

WordPress

コメントありの記事

記事の投稿IDなどの投稿者(ユーザー)情報を取得する方法 → get_the_author_meta

「アバターを取得する「get_avatar」についても追記しました。」

WordPress

jQueryでformを操る【nameの値を取得する/クリアする】

「「値をクリアする」を追加しました。チェックボックスやラジオボタンの値を何も選択していない状態に戻すときについて触れています。」

JavaScript

iframeの高さを自動調整する

「>匿名さん コメントありがとうございます。 サンプルでは、beforeは何も処理していないので期待する表示にならない、で正解です。 クリックした後が、対応・・・」

JavaScript

Instagram API を使ってインスタに投稿した画像データを取得する

「コメントに嬉しいお言葉、ありがとうございます!」

API

MAMPでMySQL Serverが突然起動しなくなった時 → ログファイル削除

「(匿名)さん コメントありがとうございます。何も設定変更していないのに、それまで使用できていたサーバーが突然使えなくなるのは辛いですよね、、。 ここで紹介したや・・・」

MAMP

イラストレーターでレイヤーごとにPNGで書き出す

「コメントありがとうございます! そもそもできるかはわかりませんが、ソースを書き換える必要はあります。 下記記事などが参考になるかもしれません。 illustra・・・」

Illustorator

05_各種キーワードツール

「公開から14年振りの更新w デッドリンクを削除しました。ツール系は「SEO対策に役立つ便利ツール」に集約します。」

SEO

WP_Queryを使って絞り込み検索するのにコレは便利!

「「特定の「タイプ」に関連付けられた投稿を表示する場合」を一つの場合を追記し、複数の場合と分けました。」

WordPress

ホーム画面からの起動か判別する

「コメントありがとうございます。 この記事で紹介している機能は、記事内のサンプル「ホーム画面からの起動か判別する」というリンクをクリックすることで確認できます。 ・・・」

JavaScript

Javascriptでカレンダーを作ってみた

「JWOLさん> 祝日名を追加されるとのこと、いいですね!コメント&参考にしていただきありがとうございます!」

JavaScript

WEB制作マークアップJavaScriptiOS Safariで絶対配置(position:fixed)して惰性スクロールしてもロックしない、フリーズ回避方法 | シンプルシンプルデザイン