まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:ios

ブラウザ内で動作するウェブアプリケーションの中で「別ウィンドウで画面を開く」という操作をしたい場合に大きく以下の2つの方法があります:
(1)target="_blank" 属性のついた <a> タグをクリック/タップする
(2)JavaScript の window.open 関数を実行する


より一般的な方法は(1)だと思っています。リンク時に使う <a> タグに target="_blank" という属性を含めておくことで常に新しいブラウザウィンドウを(ブラウザによっては別タブを)開いて、その新ウィンドウの中でリンク先ページが表示されるというものです。比較的簡単に実装できる方法です。

※ <a> タグの target 属性は本来は「ウィンドウを指定して開く」機能です。例えば <a target="abc" ..> というタグをクリックすると abc という名前のウィンドウを探して(存在していない場合は新ウィンドウを作って abc という名前で管理して) abc ウィンドウの中で目的先のリンクを開きます。再度同じ <a> タグをクリックすると、(ユーザーが消していない限り)既に abc ウィンドウは存在しているのでその abc ウィンドウの中で目的のリンクが再び開く、という動作をします。 ただし taget="_blank" という指定があった場合のみ例外的に「常に新しいウィンドウを開く」という挙動になります。


一方の(2)は「 JavaScript の処理の一環として新しいウィンドウを指定した属性と URL で開く」という関数を使って実現する方法です。利用者から見た挙動はあまり変わらないのですが、内部的には「ポップアップ」という扱いになり、違いは多くあります:
・ウィンドウサイズが指定できる(わざと小さいウィンドウで開く、といったこともできる)
・スクロールバーやメニュー、アドレスバーの有無などを指定できる
・タブブラウザであっても新しいウィンドウを作って開く
・JavaScript だけで実現できる(ライブラリ化できる)

例えば SDK などの機能をライブラリとして提供する側の立場で新しいウィンドウで何かを表示する、という機能を実現しようとすると、前述の <a> タグを使う方法は(利用者がわざわざ指定どおりの <a> タグを記述しないとできないため)逆に不便だったりします。一方で JavaScript だけでできるこの方法であれば SDK の一部としてその機能を実装しておき、その機能ごと JavaScript ライブラリを提供すればよいので利用者の負担を軽くすることができます(結果的にサポートの負担も減ると思われます)。

といった違いがあって、実装の違いにも現れることになります。


さて、この後者の方法は「ポップアップ」として扱われると書きましたが、このポップアップは特に iOS の Safari ブラウザでは扱いが面倒なものの1つです。具体的にはポップアップ機能自体がデフォルトでオフになっており動きません(つまり上述の window.open で別ウィンドウを開こうとしても開きません)。ここまでは他のブラウザでも同様なのですが、他ブラウザの場合はポップアップをしなかった旨を画面に通知して(そこで利用者は気づくことになって)ポップアップを許可することができ、次回以降の window.open 実行時にポップアップウィンドウが表示できるようになります。しかし iOS の Safari ブラウザではポップアップをしなかったことをユーザーに通知することもないため、ユーザーからすると「何も起こらなかった」ようにしか見えない(エラーメッセージが表示されるわけでもないため、許可すればよいと気づくこともない)のでした。


iOS Safari でポップアップが開くようにするには、あらかじめ Safari アプリケーションの設定でポップアップを許可する(ポップアップブロックを無効にする)必要があります。以下その手順の紹介をします:


iOS Safari でポップアップブロックの設定を変更するには「設定」-「Safari」を選択し、「ポップアップブロック」と書かれた設定項目を探します。過去に一度も変更したことがなければ下図のように「ポップアップブロックは有効(= window.open では新しいウィンドウは開かない)」になっているはずです:
2020031601


ポップアップウィンドウを表示するよう変更したい場合は、この設定を下図のように OFF に切り替えます。設定はこれだけです:
2020031602


この状態で再度 Safari を使って window.open が実行される状態を作ると、まず以下のような確認ウィンドウが表示されます。ここで「許可」をタップすると初めて window.open が実行されて新しいウィンドウが開いて処理を続けることができるようになります:
2020031603



なお、ここで書かれた手順を実行しておくと、先日のブログエントリで紹介した LIFF の新機能を実装したアプリケーションを iOS の Safari からも実行することができるようになります(つまりポップアップを有効にしないと LIFF の shareTargetPicker が動かないようです):
http://dotnsf.blog.jp/archives/1077179113.html


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

「iPhone でゲームボーイアドバンスのゲームがプレイできる」というフレコミの GBA4iOS というアプリのバージョン 2.0 がリリースされた、というニュースがありました。

いわゆる「エミュレータ」というやつで、ここでその是非についてコメントするつもりはありません。脱獄した iPhone 向けには以前から提供されていたもので、その意味では特別目新しいニュースでもありません。

が、今回ちょっと気になったのは、「脱獄不要である」という点です。脱獄不要ということは、そのアプリは App Store からダウンロードするということ? アップルがそんなアプリの審査通すの? と。

といった好奇心から、自分でもこのアプリをインストールしてみました。結論としては確かに iPhone にネイティブアプリとしてインストールできているようです。
2014022102


でも導入時に App Store は経由してません
。当たり前だよな、Apple がこんなの許可するわけがない。また(この機種に限ってはw)脱獄もしてません。 アプリ開発者が App Store を経由せずにテストアプリを導入する方法はあるんですが、その場合は電話番号とか iPhone の UDID とかを入力する面倒な手間があります。今回はそういうのも聞かれてないので、ちと違う気がする。 一体どういうこと?? どこからどんな仕組みでこのアプリはインストールされたんだ??


で、他の作業で VPN の設定とかをしていてふと気付いたのが、設定 - 一般 - プロファイル の中に「プロビジョニングプロファイル」として GBA4iOS という、そのまんまの名前のプロファイルが追加されていました。これは??
2014022101


・・・プロビジョニングプロファイルか、ああ、なるほど。分かった気がする。おそらく、以前にちょっと話題になった「iOS デベロッパーエンタープライズプログラムに抜け穴があった」という件に関係してるんだろう。

要は、自社とか特定の企業向けにアプリケーションを提供したい、という企業に対して、Apple はその企業専用のミニ App Store もどきを用意できるようにして、そこから特定ユーザーだけにアプリケーションをインストールさせる、というのが iOS デベロッパーエンタープライズプログラムです(詳しくはこちら)。そこにセキュリティホールというか抜け道があった、というものでした。それを応用(?)して制約なしに誰もがそのミニ App Store もどきを使えるようにしたんだろうな。 
念のため補足すると、このデベロッパーエンタープライズプログラムは契約時に利用条項に同意する必要があり、その中で不特定多数に向けてアプリを提供することは禁止されているので、Apple にすれば「契約違反」ということになるんでしょう。ただその抜け道の存在は想定外だったのだろうな、と。

その後もイタチごっこのような状況が続いていますが、今回の GBA4iOS 2.0 がリリースされた所をみると完全に防ぐのが難しい、ということなのかもしれません。



 

このページのトップヘ