2017年11月4日土曜日

iPhoneXかどうかをJavaScriptで判断するには?

まずUserAgentで判別できないかと考えた。しかし、

mozilla/5.0 (iphone; cpu iphone os 11_1 like mac os x) applewebkit/604.3.5 (khtml, like gecko) mobile/15b93

という感じなので無理。

cordova-plugin-deviceでもダメ。


で、世間ではどのような議論が行われているかというと、

ios - Detect if the device is iPhone X - Stack Overflow

画面の幅高さで判別しろとか、ネイティブ側で

struct utsname systemInfo;
uname(&systemInfo);

的なことをしろとか、safe-area-inset-*があるかどうかで判別しろとか、そういった議論が行われている。

今回はJavaScriptで判別する必要があるので、ネイティブ側から情報を取得する方法は使えない。

画面の幅高さで判別する方法は、現状では問題無いだろうが、端末ごとに幅高さが変更される可能性があることを考えると、あまり上手い方法とは言えない。

safe-area-inset-*があるかどうかで判別するのは有効そうだ。safe-area-inset-*を持つということは「ノッチ」がある端末ということなので、iPhoneXに限らずEssential Phoneなども検出できるだろう。

問題はJSからどのようにsafe-area-inset-*を取得するかだ。

WEBページをiPhoneXに対応させる方法として、CSSで「constant(safe-area-insets-*)」を使うということはわかっているが、これをJSで取得するにはどうするのか?

window.screenやdocument.documentElement.styleのプロパティーをくまなく見てみたが、safe-area-inset-*を取れそうなプロパティーは存在しなかった。

そこで、safe-area-inset-*のpaddingを持つダミーエレメントをテンポラリにDOMツリーに追加し、値を取得した後すぐに削除するというdirtyな実装をした。

/**
 * セーフエリアがあるディスプレイかどうかを返す
 */
function isSafeAreaDisplay() {

  var flag = false;

  //constant()はiOS 11.2で削除されるのでenv()へのフォールバックが必要
  //Designing Websites for iPhone X | WebKit
  //https://webkit.org/blog/7929/designing-websites-for-iphone-x/
  var isSafeAreaConst = CSS.supports('padding-left: constant(safe-area-inset-left)');
  var isSafeAreaEnv = CSS.supports('padding-left: env(safe-area-inset-left)');

  //どちらかがサポートされているなら実際の値を計算
  if (isSafeAreaConst || isSafeAreaEnv) {

    //safe-area-inset-*をダミーDIVのpaddingに設定する
    var elmDiv = document.createElement('div');
    if (isSafeAreaConst) { //iOS 11.1
      elmDiv.style.paddingTop = 'constant(safe-area-inset-top)';
      elmDiv.style.paddingLeft = 'constant(safe-area-inset-left)';
      elmDiv.style.paddingRight = 'constant(safe-area-inset-right)';
      elmDiv.style.paddingBottom = 'constant(safe-area-inset-bottom)';
    } else { //iOS 11.2
      elmDiv.style.paddingTop = 'env(safe-area-inset-top)';
      elmDiv.style.paddingLeft = 'env(safe-area-inset-left)';
      elmDiv.style.paddingRight = 'env(safe-area-inset-right)';
      elmDiv.style.paddingBottom = 'env(safe-area-inset-bottom)';
    }

    //非表示にしてドキュメントに追加
    elmDiv.style.display = 'none';
    document.body.appendChild(elmDiv);

    //計算後のsafe-area-inset-*を取得
    var elmDivStyle = window.getComputedStyle(elmDiv);
    var safeAreaInsetTop = parseInt(elmDivStyle.paddingTop);
    var safeAreaInsetLeft = parseInt(elmDivStyle.paddingLeft);
    var safeAreaInsetRight = parseInt(elmDivStyle.paddingRight);
    var safeAreaInsetBottom = parseInt(elmDivStyle.paddingBottom);
    console.log(
      'safeAreaInsetTop=' + safeAreaInsetTop,
      'safeAreaInsetLeft=' + safeAreaInsetLeft,
      'safeAreaInsetRight=' + safeAreaInsetRight,
      'safeAreaInsetBottom=' + safeAreaInsetBottom
    );

    //セーフエリアディスプレイか判断
    if (window.navigator.userAgent.indexOf('iPhone') != -1) {

      //iOSの場合、iPhoneXでない端末の場合でもステータスバーの20pxがinsetに設定されるので、このケースを除外する必要がある
      var statusBarH = 20;

      //デバイスの回転によってどこにinsetが付くのかわからないので、4辺のどこかにinsetが設定されればセーフエリアディスプレイだとみなす
      if (safeAreaInsetTop > statusBarH || safeAreaInsetLeft > statusBarH || safeAreaInsetRight > statusBarH || safeAreaInsetBottom > statusBarH) {
        flag = true;
      }

    } else {
      if (safeAreaInsetTop > 0 || safeAreaInsetLeft > 0 || safeAreaInsetRight > 0 || safeAreaInsetBottom > 0) {
        flag = true;
      }
    }

    //ドキュメントから削除
    document.body.removeChild(elmDiv);

  }

  //デバッグログ
  console.log('isSafeAreaDisplay=' + flag, 'isSafeAreaConst=' + isSafeAreaConst, 'isSafeAreaEnv=' + isSafeAreaEnv);

  return flag;

}

もちろん、isSafeAreaDisplay()だけではEssential Phoneなどの可能性もあるので、iPhoneXかどうかを判別するにはやはり幅と高さも考慮する必要があるが、「ページをノッチがある端末に対応させる」という目的なら、isSafeAreaDisplay()だけでも事足りるだろう。

iPhoneXかどうかを判断するには、ここまでやる必要がある。

/**
 * iPhoneXかどうかを返す
 */
function isIPhoneX() {

  //iOSかどうか?
  var isIOS = false;
  if (window.navigator.userAgent.indexOf('iPhone') != -1) {
    isIOS = true;
  }

  //幅と高さはiPhoneXサイズか?
  //デバイスのピクセルサイズは(1125,2436)だが、
  //JSから見た幅と高さは3分の1の(375,812)であることに注意
  //Adaptivity and Layout - Visual Design - iOS Human Interface Guidelines
  //https://developer.apple.com/ios/human-interface-guidelines/visual-design/adaptivity-and-layout/
  var isDimensionOK = false;
  if (window.screen.width == 375 && window.screen.height == 812) {
    isDimensionOK = true;
  }

  //デバッグログ
  console.log('isIOS=' + isIOS, 'isDimensionOK=' + isDimensionOK, 'isSafeAreaDisplay=' + isSafeAreaDisplay());

  return isIOS && isDimensionOK && isSafeAreaDisplay();

}

少々微妙な感じではあるけれど、ここまでチェックすれば、まぁiPhoneXと言えるのではないだろうか。