2020 年最初のブログとなりました。本年もよろしくおねがいします。


自分が作って公開するアプリの中にスマホのモーションセンサーを使うものが少なからず存在しています。モーションセンサーとはスマホの傾きや加速度を3軸(3次元)で取得するもので、この値を取得することで「スマホは今どんな姿勢なのか?(まっすぐなのか、傾いているのか、傾いているとしたらどの方向にどのくらい傾いているのか、・・)またどのような挙動をしているのか?(止まっているのか、動いているのか、動いているとしたらどの方向にどのような加速度を持って動いているのか、・・)」といった情報を取得することができ、これによってスマホをコントローラーのように扱うことができるようになるものです(例: 左に傾けたら左矢印が押された時と同じ挙動にする、など)。いろいろ面倒なネイティブアプリケーションを作らなくても、ウェブの(HTML と JavaScript の)アプリケーションでもセンサー情報が取得ができて VR アプリケーションみたいなものも作れる、といった便利さがありました。

ただ最近になって、少し前に作ったモーションセンサー対応アプリが現在のスマホでは動かなくなっていることに気づきました。これはデバイスそのものの問題ではなく、OS(iOS や Android)側のセキュリティ強化によるものが原因でした。昔の OS を使っている場合はセンサーがそのまま動くけど、新しい OS を使っていると同様には動かない、といった現象に遭遇するわけです。

これはまあ OS 提供側の事情もあるし、動かなくなったからといって不満ばかり言っていても動くようになるわけでもありません。今後の変更も含めてアプリケーション提供側が対応していくしかないのかなあ、、と半ばあきらめています。

#ただ1つだけ不満を言わせてもらうと、Android はともかく iOS 側の変更の頻度が高すぎて「やってられん!」という気になってしまうのも事実です。 (^^;


といった背景の中で、とりあえず 2020 年1月時点での OS(とバージョン)別モーションセンサー有効化方法をまとめてみました。


【OS/バージョン別対応策】

詳細は後述しますが、対応策の一覧はこちらになります:
OSバージョン対応策
Android9 以上(おそらく全バージョン)不要
以下の【モーションセンサーから情報を取得する JavaScript】の内容を実装すれば取得できる
iOS12.1 以下
12.2 以上 13 未満Safari の設定を変更
13 以上アプリケーション側で requestPermission を実行するよう実装し、ユーザーに「許可」させる


※2020/10/29 追記
iOS 14.0.1 での動作を確認しました。上記「13 以上」の対応内容で取得できました。


【モーションセンサーから情報を取得する JavaScript】
まず全ての OS やバージョンに関係なく、モーションセンサーから情報を取得する JavaScript の実装を紹介します(iOS 12.2 以上の場合は、この JavaScript を実行する前に後述の対応策が必要となります)。

例えばデバイスの姿勢(3次元軸での向き)を取得するには DeviceOrientation オブジェクトを使います(デバイスの挙動・加速度を取得するには DeviceMotion オブジェクトを使いますが、内容は同様なので省略します)。このオブジェクトはジャイロセンサー搭載マシンのブラウザでは有効になっています(ノート PC などジャイロセンサー非搭載機では無効です)。

この DeviceOrientation オブジェクトから値を取得するには deviceorientation イベントに対するハンドラを定義し、ハンドラ内で値を取得する必要があります。具体的には以下のようになります:
  :

//. DeviceOrientationEvent オブジェクトが有効な環境か? をチェック
if( window.DeviceOrientationEvent ){
  //. DeviceOrientationEvent オブジェクトが有効な場合のみ、deviceorientation イベント発生時に deviceOrientaion 関数がハンドリングするよう登録
  window.addEventListener( "deviceorientation", deviceOrientation );
}
  :

//. deviceorientation イベントハンドラ
function deviceOrientation( e ){
  //. 通常の処理を無効にする
  e.preventDefault();

  //. スマホの向きを取得
  var dir = e.alpha;   //. 北極方向に対する向きの角度
  var fb = e.beta;      //. 前後の傾き角度
  var lr = e.gamma;  //. 左右の傾き角度

    :
}

  :

まず window.DeviceOrientationEvent というオブジェクトが有効かどうか(モーションセンサー対応デバイスかどうか)をチェックします。有効な場合はデバイスから定期的に deviceorientaion イベントが発呼されるので、そのイベントが発生した時に呼ばれる関数(ハンドラ) : deviceOrientation() を定義しておきます。

deviceOrientation() 関数はハンドラが呼ばれた時点での情報を持つオブジェクト e を引数として実行されますが、このオブジェクトは3つの要素を持っています。e.alpha が北極方法に対するスマホの向き、e.beta が前後の傾き、そして e.gamma が左右の傾きで、全て角度(単位は度)の値が含まれています。これらの値を取り出すことで、ハンドラが呼ばれたタイミングでのこれらの角度を知ることができるようになる、というものです。なお、deviceOrientation() 関数は1秒に数十回呼び出されます。かなり細かい頻度でデバイスの動きを知ることができるようになっています。


上述の表でも説明しましたが、Android および iOS 12.1 以下であれば上記 JavaScript が記述されたページを HTTPS で開けばそのまま実行できてセンサー値を取得することができるようになります。

iOS 12.2 以上の場合は上記 JavaScript が実行される前に準備的な段階が必要になります。iOS 13 未満か以上かで準備段階の内容が異なるため注意が必要です。


【iOS 12.2 以上 13 未満での事前準備】
iOS 12.2 以上 13 未満の場合(つまり 12.x で x が2以上の場合)はデフォルトで Safari からセンサー値を取得することができないように設定されているため、この設定を変更しておく必要があります。

具体的には iOS の設定 - Safari に「モーションと画面の向きのアクセス」という項目があります。デフォルトではこの設定は OFF になっているはずですが、ここを ON に(緑色になるように)変更しておく必要があります:
2020011001


この変更をしておくだけで、後は Safari を開いて上述の JavaScript が含まれるページを開けば正しく実行され、センサー値を取得することができます。


【iOS 13 以上での事前準備】
さて問題の iOS 13 以上(現在 iOS を普通にアップデートするとこの状態になります)でのケースです。この環境下では前述のような Safari の設定は不要ですが、代わりにユーザーの許可無しに Safari からセンサー値を取得することができないように設定されています。なおこの許可はウェブページごとに許可する必要があるため、事前に Safari で全ページ向けの設定をしておく、ということ自体ができなくなりました。

またセンサー値を取得するページ(の JavaScript)側も個別にユーザーの許可を得るための JavaScript コードを記述して対応する必要があります:
2020011002
(↑このダイアログを出して、「許可」が選ばれないとセンサー値は取得できない)


しかも(まだありますw)この「個別にユーザーの許可を得る」タイミングも面倒です。サービスを提供する側としては「該当の URL を指定してページを開くと同時にユーザーの許可を得たい」と思うわけですが、これが許されていません(汗)。該当ページを開ききって、ユーザーがそのページの中にあるボタンをタップしたタイミングで許可を得るためのダイアログを表示し、許可された場合のみ DeviceOrientationEvent のイベントハンドラが有効になる、という仕様に変わったのでした。正直、開発・運用する側としてはかなり面倒な仕様になってしまいました。。


具体的なコードは以下のようになります。青字部分が元のコードからの変更箇所です:
  :

function ClickRequestDeviceSensor(){
  //. ユーザーに「許可」を求めるダイアログを表示
  DeviceOrientationEvent.requestPermission().then( function( response ){
    if( response === 'granted' ){
      //. 許可された場合のみイベントハンドラを追加できる
      window.addEventListener( "deviceorientation", deviceOrientation );
      //. 画面上部のボタンを消す
      $('#sensorrequest').css( 'display', 'none' );
    }
  }).catch( function( e ){
    console.log( e );
  });
}

//. DeviceOrientationEvent オブジェクトが有効な環境か? をチェック
if( window.DeviceOrientationEvent ){
  //. iOS13 以上であれば DeviceOrientationEvent.requestPermission 関数が定義されているので、ここで条件分岐
  if( DeviceOrientationEvent.requestPermission && typeof DeviceOrientationEvent.requestPermission === 'function' ){
    //. iOS 13 以上の場合、
    //. 画面上部に「センサーの有効化」ボタンを追加
    var banner = '<div  style="z-index: 1; position: absolute; width: 100%; background-color: rgb(0, 0, 0);" onclick="ClickRequestDeviceSensor();" id="sensorrequest"><p style="color: rgb(0, 0, 255);">センサーの有効化</p></div>';
    $('body').prepend( banner );
  }else{
    //. Android または iOS 13 未満の場合、
    //. DeviceOrientationEvent オブジェクトが有効な場合のみ、deviceorientation イベント発生時に deviceOrientaion 関数がハンドリングするよう登録
    window.addEventListener( "deviceorientation", deviceOrientation );
  }
}
  :

//. deviceorientation イベントハンドラ
function deviceOrientation( e ){
  //. 通常の処理を無効にする
  e.preventDefault();

  //. スマホの向きを取得
  var dir = e.alpha;   //. 北極方向に対する向きの角度
  var fb = e.beta;      //. 前後の傾き角度
  var lr = e.gamma;  //. 左右の傾き角度

    :
}

  :

ユーザーにセンサーデータ取得の許可を求めるには DeviceOrientationEvent オブジェクトの requestPermission() 関数を実行します(この関数は iOS13 以降でのみ有効なので、iOS13 未満や Android では実行できません)。そしてそのダイアログで「許可」が選ばれた場合のみイベントハンドラを有効にすることができるようになるので、addEventHander で deviceOrientation() 関数を登録することでセンサー値を取得することができるようになる、というものです。

また requestPermission() 関数は全ての画面がロードされきった後でのみ実行できます。そのため、まず iOS13 以降かどうかを判断し、そうであった場合の画面ロード時にはセンサー値取得の許可を求めるためのダイアログを表示するボタンを用意し、そのボタンがタップされたタイミングで DeviceOrientationEvent.requestPermission() を実行して許可を求めるダイアログを表示して、「許可」が選ばれた場合は deviceOrientaion() 関数を有効にする、という順序でセンサー値を取得しています:
2020011002



・・・というわけで、特に iOS 13 以降で面倒になったブラウザでのモーションセンサー有効化を手順を含めて解説しました。現実問題としてはここまで実装した上でインターネット上のサーバーに公開してはじめて動かすことができるようになるまでも大変だし、モバイルブラウザだとデバッグも大変なので超面倒だなあ、という印象です。 ただ近い将来にこの機能を使って実装したサービスを公開するつもりでいるので、事前にややこしい所だけを説明しておく目的でこのブログエントリを作ったのでした。