misallychan.com

雑記。はてブ、口コミ、WEBデザイン、アニメ・漫画についてとか。

iPhoneでスクロールが止まらない!iOSでモーダル背景のスクロールを無効にする方法



年末じゃ。忙しいぞ。


さて。
モーダルウィンドウやポップアップ、ドロワーメニューを開いた際など。
背景までスクロールされてしまって困ったことはないでしょうか。
ちなみにこちらの現象、色々対応できるのですがiOSだけ、なかなかうまくスクロールを無効にできません。


f:id:misallychan:20181227181826p:plain

iOSだけモーダル背景がスクロールされてしまう

iOSだけ、なんでスクロールを止められないんだ…。。。
検証もしにくいし、本当に困り果てていました。
色々調べて試してみても、なかなか治らず大苦戦…。


気に病みすぎてうまくいかず、毎日心が荒んでいきました。
昔からwordpressのカスタマイズとかでも悩み、こういうのちょっとうまくいかないと、めちゃめちゃ気に病む癖が^^;



overflow:hiddenやpreventDefaultでは?

例えば、overflow: hidden。
これだけでは、iOSではスクロールを無効にできません。


有益そうな情報で、iOSのスクロール対策として出てきた
touchmoveのときにnoScrollでpreventDefaultでも、うまく無効にできませんでした。
一度うまくいったかと思って指を動かしまくると、やっぱり動いてしまうのです…。



スクロール停止できる!解決策を発見

ですが。
色々試した中で、これだ!!という記事がありました。
jQuery スマートフォンでスクロールを停止させたい方へ。 | チルチルミチルブログ


心の底から本当〜〜〜〜に感謝。
こちらのコードで試したとき、初めてiOSでもスクロールが止まってくれたのです。




話は逸れますがそれにしてもこの参考にさせていただいた記事の会社、とても素敵です。
前職でコーポレートサイトとか作る機会が多かったので、そのときに参考にしたかった…。
はじめてホームページを作る方必見!作る前に知って欲しい制作ガイド
こちらのガイド、ものすごく親切に、サイトに関する知識がない人でもわかりやすく説明されてる。
かわいいイラストやわかりやすい画像と一緒に。
費用も明確だしSEOについても書いてある…。
この会社になら任せたくなるような打ち出しですごい見惚れる。。


でも、モーダル内でもスクロールが止まってしまう

さて…。。。
こちらが唯一の希望でしたが、こちらを参考にするだけだと、私にとっては一つ問題がありました。


それは、ページ全体のスクロールが止まってしまうこと。


私が実装したかった理想は以下。

今回の実装の条件

(1)モーダル内でスクロールが必要な場合があるので、モーダル内だけはスクロールしたい
(2)でも、モーダルを閉じた際に元の場所にいてほしいので、背景は固定しておきたい



というわけで、全体がスクロールできなくなってしまうと、
画面よりモーダルの内容が大きかった場合にスクロールできず、困ってしまったわけです。




背景は固定し、画面の高さよりモーダル内の内容が大きい場合のみ、スクロールさせる



今回の理想として、
「背景は固定し、画面の高さよりモーダル内の内容が大きい場合のみ、スクロールさせる」
ことを実現したい。


そこでこちらを参考に、追記してみたのが以下になります。

html
<div id="wrap">
    ここに背景、中身の記載
</div>

<div id="modal">
    <div class="modal_innner">
        モーダルの内容を記載
    </div>
</div>

まずは、背景とモーダルを切り分けたいので、背景「#wrap」の外に、モーダル「#modal」を置きました。


jQuery
$(function() {
    var $window = $(window),
        $html = $('html'),
        $body = $('body');
    var wrap = document.getElementById('wrap'); // 背景
    var movefun = function( event ){ event.preventDefault();  };
    var ua = navigator.userAgent; // ユーザーエージェント
    var wH = $(window).height(); // 画面の高さ取得

//モーダルを開く動作を入れる
    var mH = $('.modal_inner').innerHeight(); // モーダルの高さ取得

	if (ua.indexOf('iPhone') > 0 || ua.indexOf('iPad') > 0) { // iOSの場合のみ
		if(mH > wH) {
			// 画面の高さよりモーダルの高さが大きく、モーダル内スクロールが必要な場合
			wrap.addEventListener( 'touchmove', movefun ,{ passive: false });
			$('html, body').css('overflow', 'hidden');
		}else{
			// 画面の高さよりモーダルの高さが小さく、モーダル内でスクロールの必要がない場合
			window.addEventListener( 'touchmove', movefun ,{ passive: false });
			$('html, body').css('overflow', 'hidden');
		}
	} else { // iOS以外
		wrap.addEventListener( 'touchmove', movefun ,{ passive: false });
		$('html, body').css('overflow', 'hidden');
	}

//モーダルを閉じる動作を入れる
    wrap.removeEventListener( 'touchmove' , movefun, { passive: false } );
    window.removeEventListener( 'touchmove' , movefun, { passive: false } );
});




注意事項

というわけで、なんとなくできた感があるのですが…。
結論から言うと、「完璧に背景のみスクロールを止める」ことは無理でした。
なぜか、下の方をスクロールするとどうしても動いてしまったりなど、出てきます。



いろいろ試した中で、今回が一番許容範囲内の動きをしてくれましたので、詳細に解説していきます。
思いっきりスクロールしまくるような、滅多なことがなければ、こちらでまあ大丈夫かと思っています。


簡単にコード解説。

私はjQueryのことはよくわからないので、一応動作としては動いたものの、
表現など間違ってる部分もあるかもしれません…。
あらかじめご了承ください!




step1

まず最初の記事を参考に

window.addEventListener( 'touchmove', movefun ,{ passive: false });

こちらがあれば、ページ全体のスクロールが止まることを確認できました。


step2

そこで、全体じゃなく背景のスクロールだけを止めるように指示を出したいと思いました。

var wrap = document.getElementById('wrap'); 
wrap.addEventListener( 'touchmove', movefun ,{ passive: false });

色々試してみたのですが、この「document.getElementById」っていうのであれば、windowの部分を書き換えても動いてくれました。
これで、「#wrap」の部分をスクロール止めたいですよってしてます。(日本語〜)


こちらでモーダル内のスクロールはうまくいくようになりました。



ただ、これだとモーダル内のスクロールはなんとなくうまくいったものの、
モーダルの中身が画面サイズの縦幅よりも小さい場合(モーダル内にスクロールを必要としない場合)にはなぜか
じゃんじゃんにスクロールできてしまって、うまくいきませんでした。



step3

整理します。
・モーダル内スクロールが必要な場合
→このままでうまくいく
・モーダル内スクロールの必要がない場合
→このままだとうまくいかない

「モーダル内スクロールの必要がない場合」はページ全体のスクロールが停止しても問題がありません。
なので、この場合だけもともとの「window」でページ全体のスクロールを止めてしまおうと思いました。


step4
var wH = $(window).height(); // 画面の高さ取得
var mH = $('.modal_inner').innerHeight(); // モーダルの高さ取得

まずは、画面の高さを取得。
そして、モーダルを開いた後に、モーダルの高さも取得します。


step5
var ua = navigator.userAgent; // ユーザーエージェント
if (ua.indexOf('iPhone') > 0 || ua.indexOf('iPad') > 0) {
}

今回問題が起こるのがiOSの場合のみなので、
ユーザーエージェントでiOSの場合のみ分岐することにしました。
[ jQuery ] ユーザーエージェント判定 - Qiita
こちらの記事を引用させていただきました。


step6

で、iOSの場合のみこちらです。

if(mH > wH) {
	// 画面の高さよりモーダルの高さが大きく、モーダル内スクロールが必要な場合
	wrap.addEventListener( 'touchmove', movefun ,{ passive: false });
	$('html, body').css('overflow', 'hidden');
}else{
	// 画面の高さよりモーダルの高さが小さく、モーダル内でスクロールの必要がない場合
	window.addEventListener( 'touchmove', movefun ,{ passive: false });
	$('html, body').css('overflow', 'hidden');
}

mH(モーダルの高さ)> wH(画面の高さ)の場合は、モーダルの高さがあるのでモーダル内ではスクロールが必要です。
そこで「wrap.add〜〜〜」としています。


で、先程こちらだけだとモーダルの高さがないときにスクロールできてしまってうまく動かなかったので、それ以外の場合としてelseで分けています。


それ以外の場合というのが
mH(モーダルの高さ)< wH(画面の高さ)の場合のことです。
モーダルの高さがないのでモーダル内ではスクロールが不要、ページ全体でスクロールを停止させても問題ありません。
そこで「window.add〜〜〜」としました。


step7

そしてiOS以外の場合を記載。

wrap.addEventListener( 'touchmove', movefun ,{ passive: false });
$('html, body').css('overflow', 'hidden');

背景以外のスクロールを止めるようにします。



step8

最後に、モーダルを閉じる際の動作を忘れずに。

wrap.removeEventListener( 'touchmove' , movefun, { passive: false } );
window.removeEventListener( 'touchmove' , movefun, { passive: false } );

モーダルを閉じた後は、背景も全体もスクロールしてほしいので
「remove」で、touchmoveの際のpreventDefault()を外します。



画像取得の場合の問題

モーダルを開いたときに画像を読み込む場合、画像の読み込みに時間がかかってモーダルの高さが読み取れませんでした。

ですので、例ですが、先に画像の高さを取得してから、画像の高さによって分岐をすることにしました。



html
<div id="wrap">
    ここに背景、中身の記載
</div>

<div id="modal">
    <div class="modal_innner">
        モーダルの内容を記載
        今回は画像を表示させます。
            <img src="" data-imgh="画像の高さを書きます">
    </div>
</div>
jQuery
$(function() {
    var $window = $(window),
        $html = $('html'),
        $body = $('body');
    var wrap = document.getElementById('wrap'); // 背景
    var movefun = function( event ){ event.preventDefault();  };
    var ua = navigator.userAgent; // ユーザーエージェント
    var wH = $(window).height(); // 画面の高さ取得

//モーダルを開く動作を入れる
    var img = $(this).children('img'); // モーダル内の画像の取得
    var imgH = $(img).attr('data-imgh'); // 画像の高さ取得

			if (ua.indexOf('iPhone') > 0 || ua.indexOf('iPad') > 0) { //iOS対策
				if(imgH > 1200) { //縦長画像の時は背景スクロール
					wrap.addEventListener( 'touchmove', movefun ,{ passive: false });
					$('html, body').css('overflow', 'hidden');
				}else if(520 > wH) { //「画面」の高さが520より小さい時は背景スクロール
					wrap.addEventListener( 'touchmove', movefun ,{ passive: false });
					$('html, body').css('overflow', 'hidden');
				}else{ //それ以外(横長画像)は画面すべてスクロール止める
					window.addEventListener( 'touchmove', movefun ,{ passive: false });
					$('html, body').css('overflow', 'hidden');
				}
			} else { //iOS以外
				wrap.addEventListener( 'touchmove', movefun ,{ passive: false });
				$('html, body').css('overflow', 'hidden');
			}

//モーダルを閉じる動作を入れる
    wrap.removeEventListener( 'touchmove' , movefun, { passive: false } );
    window.removeEventListener( 'touchmove' , movefun, { passive: false } );
});


今回の場合は画像の高さが1200pxより大きい場合は縦長画像とし、縦長画像の場合はモーダル内のスクロールが必要としていました。
CSSで実際に表示させる画像はレスポンシブだったり、横幅を80%などにしたら実際の表示としてはもっと小さくしています。
で、横長画像の場合はモーダル内のスクロールが必要ではありませんので、全体のスクロールを停止させます。
しかし、画面の高さが小さいときには、横長画像であってもスクロールが必要な場合がありますので、今回は520pxより画面の高さが小さい場合には全体のスクロールを停止させてしまうと困るので、背景のみスクロールとしました。



まとめ

・iOSでは背景のスクロールなど止めることが難しい
・ただ全体のスクロールを止めることはできる
・色々と分岐わけすることで理想の動きに近づいた



以上になります。


とりあえず、こちらで動くはずなのですが…。。
今回こちらに載せる用に、編集したものですので、足りない部分などありましたら補って頂けると嬉しいです。
そして、他にも方法あれば教えてほしいです。どうぞよろしくお願いします。