2019年6月8日土曜日

SwiftUI 未解決問題まとめ

一通り試してほぼほぼ理解できたのですが、現状どこをどのように調べてもわからなかった内容がいくつかあるので未解決問題としておいておきます。誰か教えて\(^o^)/

Transition

type-eraseされたAnyTransitionはSwiftUI.frameworkに居るのですが、元のProtocolが見えない上にドキュメンテーションも一切ありません。おそらくはNavigationViewの中で使われているのだと思いますが詳細不明。

ScrollView / Listの現在のスクロール位置 (UIKitにおけるcontentOffset) に相当する値を参照する/コード上から指定する方法

今の所、一番怪しいのが以下のpreferenceの仕組みではないかと睨んでいるのですが、
問題は肝心要のPreferenceKeyの具体実装がSwiftUI.framework上には一切見つからず、したがってキーが指定できないためonPreferenceChange(_:perform:)がうまく利用できません。特定位置までスクロールしたら発火、とかビューが50%スクロールして隠れたら発火、とか普通に使いたいのですが、困りました。それとかあとはenvironment経由なりイニシャライザ経由なりでinitialScrollPositionのようなプロパティを用意してScrollViewのスクロール初期位置を与える、みたいなテクも使いたいですし、普通に必要だと思うんですけど(´・_・`)

ちなみにView.offsetではない・・・と思います、たぶん。一応念のためにList.offset()で試してみましたが、ドキュメンテーションにもある通り、全く違う挙動になります。

SwiftUI チュートリアル ヘルプ ドキュメント FAQ 困ったらとりあえずここ見ればOK

https://github.com/Juanpe/About-SwiftUI

いろいろ調べたのですが、これよりよくまとまっているドキュメントを現状発見できませんでしたので、2019/06/08現在では上記のドキュメントを参照するのがベストだと思います。

特に以下の記事は役立ち度が高かったです。この2つだけで問題の9割ぐらいは解決できると思います。

SwiftUI by Example
https://www.hackingwithswift.com/quick-start/swiftui

Answers to the most common questions about SwiftUI
https://wwdcbysundell.com/2019/swiftui-common-questions/

あとは直接SwiftUI.frameworkの中身を見るのが良いと思います。正直Appleのドキュメンテーションは未だにSwiftのExtensionベースの実装をきれいにドキュメントに起こす事ができておらず、複数のドキュメントに重複した記載が見られたり、どこで定義されているfunc/varなのかを正しく表現できていなかったりします。なのでシグネチャの名前さえわかっていればSwiftUI.frameworkの中を自分で検索したほうが正確にどういう定義になっているか判断できて便利です。

2018年12月19日水曜日

HTML5 video / audioがiOSデバイスの消音スイッチの状態に従うようにする方法 (UIWebView編 / WKWebView編)

いろいろ調べていたのですが、遥か大昔に私が書いたブログ記事が長い年月を経て今や大間違いになっていたのでここに訂正させていただきたいと思います。

今回ご紹介するのはUIWebViewまたはWKWebViewで表示しているHTML5のvideo要素やaudio要素が音声を出すときに、iOSデバイスについている消音スイッチ (ミュートスイッチ, mute switch, silent switch) の状態を無視してしまう問題を解決する方法です。相変わらず紹介内容がとてもニッチですね。

ちなみにSafariはデフォルトでちゃんと消音スイッチの状態を反映してくれるので、SafariでYouTubeの動画を見ててもいきなり音が流れ出すことはありません。素晴らしい!というわけで我々のアプリもぜひそのようにしたいと思います。

消音スイッチの挙動について

まず基本的なおさらいとして、消音スイッチがどのような挙動を示すかについてこちらにまとめます。
  • 各プロセスごとに、AVAudioSessionが消音スイッチに対してどのように振る舞うかを定義している。
  • 具体的には、AVAudioSession.Categoryの値に応じて挙動が変化する。ドキュメントにも明記されている。
    • AVAudioSession.Category.ambientやAVAudioSession.Category.soloAmbientを指定すると、消音スイッチがONのときは音が出ないようになる。
    • 逆にAVAudioSession.Category.playbackを指定すると消音スイッチを一切無視するようになる。
  • 消音スイッチの現在の物理的な状態を取得するAPIは一切ない。Private APIで以前は可能だったが、穴が塞がれたためその方法も利用できない。そもそもアプリが提出時の審査で蹴られる。当然JS経由でHTMLコンテンツ上から状態を取得するのも不可能。

解決方法・UIWebView編

UIWebViewはWebKit1を利用しており、したがってUIWebViewのエンジンは我々のアプリ内のプロセスで動作します。そこでUIWebViewのインスタンスを生成するより先に、先述の通りAVAudioSessionのcategoryを変更して消音スイッチの状態を反映してやるように示してやるとうまくいきます。
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient, mode: .default, options: [])
try? AVAudioSession.sharedInstance().setActive(true)
ただしご存知の通り、すでにUIWebViewはdeprecated扱いとなっており、WKWebViewへの移行が推奨されています。そこでWKWebViewでも同様の方法が使えないかやってみましょう。

解決方法・WKWebView編

ありません。

繰り返しますが、ありません。WebKit2の設計上のバグです。

今後修正される可能性もほぼ間違いなくありません。諦めてください。

一応、順を追って説明します。
  1. WKWebViewはWebKit2で実装されています。
  2. WebKit2は我々のアプリ内のプロセスで動作するのではなく、「共用の」WebCoreプロセス上で動作し、その結果が我々のアプリ内に転送されてくるような実装になっています。
  3. 「共用の」WebCoreプロセスは(これは推測ですが、audioやvideoを最大限に活かすため)AVAudioSession.Category.playbackとAVAudioSession.Mode.moviePlaybackで動作するように設定されている用に見えます。したがって消音スイッチは無視されます。
  4. 最初にご説明したとおり、AVAudioSessionによる消音スイッチに対する振る舞いは「プロセス単位」で制御されており、これは遥か大昔のiOS 2.0どころか下手するとNeXTSTEPの時代からCarbonレイヤーで決められている挙動だったりします。要するに今からの変更はほぼ不可能です。
  5. もうおわかりだと思いますが、「共用の」WebCoreプロセスの挙動を我々個別のアプリがAVAudioSession経由で勝手に変更することは不可能ですし、これを可能にするのはiOSとWebKitの設計上実質不可能なため、問題は解決されません。
    1. WebCoreプロセスを各個別のアプリごとに立ち上げて対応しろよ!と思うかもしれませんが、WebCoreプロセスは近年AppleのOSに組み込まれている特権階級モードで動作するプロセスとなっており(そのためメモリに無制限なアクセスが可能で、JSをJITコンパイルして高速に動作させる事が可能)、最近のセキュリティとバッテリーライフにうるさいAppleがそれを個別のアプリごとに立ち上げさせるなどということは現経営陣が全滅しない限りありえないと断言できます。
    2. 共用のWebCoreプロセスが使用するAVAudioSession.Categoryを変えてしまえばいいだろ!とも思いますが、実はAVAudioSession.Categoryをplaybackに指定しない限りバックグラウンドで音楽を流し続けることができません。したがって今度はWebViewで音楽プレイヤーを作ったりPicture in Pictureで動画を流し続けるアプリが全滅するため、これも不可能です。
    3. だったら消音スイッチの状態を自分で調べて自分で動画をmuteにすればよいのでは、と思いますが、これも先述の通り消音スイッチの状態を調べる方法は一切ないため、一律でmuteにしてしまうなどの乱暴な方法を用いない限り対応できません。
やばいですね☆