iOS10のWKWebViewではビデオのインライン再生はできない→できます!
2016/10/28追記: 初回投稿時に「iOS10のWKWebViewではビデオのインライン再生はできない」として作成した記事ですが、その後の検証によりインライン再生できることが分かりました。
そのため現在の内容は初出時の内容を一部修正したものになっています。ご了承ください。
iOS 10からmobile Safariでビデオをインライン再生できるようになりました。<video>
タグにplaysinline
を指定することで、ビデオがインラインで再生されるようになります。
developer.apple.com uupaa.hatenablog.com
同じことがWKWebViewでできないかを確認してみたのですが…残念ながらまだできませんでした。ところ、同様にインライン再生出来ることが分かりました。
確認した環境は以下のとおりです。
ソースコードは以下のとおりで、WKWebViewConfiguration
を用意しそのallowsInlineMediaPlayback
プロパティをtrue
にしているのがポイントです。
let config = WKWebViewConfiguration() config.allowsInlineMediaPlayback = true // ⭐ここがポイント! let webView = WKWebView(frame: self.view.frame, configuration: config) // configと共にインスタンス化 self.view.addSubview(webview) let url = URL(string: "ここにvideo+playsinlineなURLを入力") let request = URLRequest(url: url!) self.webnavigation = self.webview?.load(request)
HTML側のコードは以下のとおり。こちらのサイトにあるコードを使わせてもらっています。
<video>
の中にplaysinline
と、念のためのwebkit-playsinline
がありますが、これがiOS10(のSafari)でインライン再生するために必要な属性です。
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no"> <meta charset="utf-8"> </head><body> <video src="http://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8", controls playsinline webkit-playsinline style="position:absolute;top:0;left:0;outline:1px solid red;width:320px"> </video> <p style="position:absolute;top:100px;left:10px;color:lime"> Hello inline player </p> </body></html>
で、これを確認するとmobileSafariと同様にインラインで再生してくれます。あれだけ苦労していたインライン再生が、いとも簡単にできてしまいました。
余談
ちなみにWKWebViewを2つ並べて再生してみたのですが、それだと片方ずつのガクガクな再生になってしまいました。高望みしてはいけませんね。
SKNodeをロケットのように震わせる
ロケットを発射したときのように、ノードをブルブル震えさせつつ、けれども元々の場所は維持するアクションはどうやって作ったらいいんだろう、と悩んでいました。 調べたところ、ここで紹介されている方法がなかなか良いことがわかりました。Swift 3で書くとこんな感じになります。
let duration = 0.5 // 繰り返しの単位 let amplitudeX = 5.0 // X方向への変化(±2.5pt) let amplitudeY = 3.0 // Y方向への変化(±1.5pt) let numShakes = Int(duration * 15.0) // 4フレーム×2を揺れの周期とする var actions = [SKAction]() for _ in 0 ..< numShakes { // 「行って」「戻る」を1アクションとして扱う let moveX = Double(arc4random_uniform(UInt32(amplitudeX))) - amplitudeX / 2 let moveY = Double(arc4random_uniform(UInt32(amplitudeY))) - amplitudeY / 2 let shakeAction = SKAction.moveBy(x: CGFloat(moveX), y: CGFloat(moveY), duration: 8.0/60) shakeAction.timingMode = .easeOut actions.append(contentsOf: [shakeAction, shakeAction.reversed()]) } // アクションの実行。actions単位の繰り返しをずっと続ける let sequence = SKAction.sequence(actions) run(SKAction.repeatForever(sequence))
少し動いてから、元に戻るという2アクションを1セットにして、それを何回も繰り返すという処理です。
参考サイト
SpriteKitでテクスチャアトラスからSKSpriteNodeを作る
SpriteKitでノード(SKNode
)に画像を使いたい場合、SKSpriteNode
をテクスチャアトラスから作るのが便利です。
通常、テクスチャアトラスを作るにはたいてい専用のツールが必要で、隙間なく上手に並べたいときなどなかなか面倒だったりします。ですがXcodeだと.xcassetsファイルがその機能を自動的に実現してくれています。
使い方は簡単です。
ファイル準備
.xcassets
ファイルを選択- アセット一覧が表示されているペインの左下部にあるプラス(
+
)ボタンをクリックして「New Sprite Atlas」を選択。テクスチャアトラスを表すフォルダ(下の画像だと右側に4つの□が並んだフォルダ)が作成される - あとは通常の画像と同じように、テクスチャアトラスに入れたい画像をそのフォルダに入れるだけ
これで、ビルドした時に自動でテクスチャアトラスを作成し、他のアセットも含んだ.car
ファイルとしてアプリにバンドルしてくれます。
スプライトの作成
テクスチャアトラスからスプライトを作る場合、テクスチャ(SKTexture
)を取り出し、そのテクスチャでスプライトを作成します。スプライトはSKSpriteNode(texture:)
メソッドで作ります。
let atlas = SKTextureAtlas(named: "Sprites") // テクスチャアトラスのフォルダ名を指定 let texture = atlas.textureNamed("pinwheel1") // スプライトに使うテクスチャを選択 let sprite = SKSpriteNode(texture: texture)
あるいは、テクスチャに使っている名前がユニークであれば、SKTexture(imageNamed:)
でいきなりテクスチャを作ることも可能です。
let texture = SKTexture(imageNamed: "pinwheel2") let sprite = SKSpriteNode(texture: texture)
細かい設定はできませんが、とても簡単に用意できるのは便利ですね。
マイクの入力レベルをdBで取得する
マイクの入力レベルを調べるのには、AVFoundationを使うのがお手軽です。下のコードは、マイクからの入力レベルをコンソール上に出力するごくごく簡単なサンプルです。
try!
を使っていたりcanAddInput/Output()
を使っていなかったりしていますので、その点は注意してください。
final class PowerLevel: NSObject { private let captureSession_ = AVCaptureSession() // オーディオデータを渡してくれるデリゲートメソッドが動くスレッド。 /* ※データ順が狂ったりメインスレッドで処理落ちしないように 「バックグラウンド」の「シリアルキュー」を必ず使うこと。*/ private let serialQueue_ = dispatch_queue_create("PowerLevel.serialqueue.audio", DISPATCH_QUEUE_SERIAL) init() { super.init() setupCaptureRoute() } func setupCaptureRoute() { // OSがデフォルトにしているマイクデバイスを選ぶ let audioDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeAudio) // マイクデバイスをキャプチャセッションにつなぐ入力クラスを用意 let audioInput = try! AVCaptureDeviceInput(device: audioDevice) // マイク入力の出力先(今回はデータをデリゲートメソッドに渡す)を用意 let audioDataOut = AVCaptureAudioDataOutput() // デリゲートオブジェクトを設定 audioDataOut.setSampleBufferDelegate(self, queue: serialQueue_) // キャプチャセッションに入出力を接続。これでいつでもstartできる captureSession_.addInput(audioInput) captureSession_.addOutput(audioDataOut) } func start() { captureSession_.startRunning() /* 開始 */ } func stop() { captureSession_.stopRunning() /* 停止 */ } } // デリゲートメソッドの用意 extension PowerLevel: AVCaptureAudioDataOutputSampleBufferDelegate { func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) { // キャプチャコネクションからオーディオチャンネルを取得し、パワーレベル(dB)を入手 let audioChannels = connection.audioChannels.filter{ $0 is AVCaptureAudioChannel } audioChannels.forEach { print("\($0.averagePowerLevel) / \($0.peakHoldLevel)") } } }
このコードはこんなふうに使います。
let obj = PowerLevel() obj.start() // コンソールにマイク入力のレベルをdB表示 obj.stop() // 処理停止
得られる値はdBで、デバイスの最大入力レベルを「0dB」とした値(つまり0に近いほど大きな値を表すマイナス値)になっています。
追記
オーディオチャンネルが複数あることも考慮すると、それぞれの平均をとったほうがよいかもです。つまり、デリゲートメソッドの中にある処理を、次のように変えます。
let audioChannels = connection.audioChannels.filter{ $0 is AVCaptureAudioChannel } if !audioChannels.isEmpty { let averagePowerLevel = audioChannels.reduce(0.0){ $0 + $1.averagePowerLevel } / Float(audioChannels.count) let peakHoldLevel = audioChannels.reduce(0.0){ $0 + $1.peakHoldLevel } / Float(audioChannels.count) print("\(averagePowerLevel) / \(peakHoldLevel)") }
画面遷移したときに下部のタブバーを一時的に消す
ナビゲーションコントローラをタブバーと組み合わせて使っている時、プッシュ遷移した先の画面ではタブバーを消して全画面表示したいケースはあります。設定が終わるまでタブバーによる遷移を避けたいときなどがそうです。遷移先から戻ってきたときには、またタブバーを表示する動作とのセットです。
その場合、「遷移元のビューコントローラ」で、「自分のhidesBottomBarWhenPushed
プロパティ」をtrue
にセットします。これで
- 遷移先に移動するときにタブバーが隠れる
- 遷移先から戻ってくるときにタブバーが再表示される
がまとめて実現できます。prepareForSegue()
で書いた場合の例を下に載せます。
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { switch segue.identifier { case "ToDetailView"?: hidesBottomBarWhenPushed = true // tabBarController?.hidesBottomBarWhenPushed = true // NG。遷移先のviewがサイズ調整されない default: break } }
ありがちなのは、遷移元ビューコントローラのhidesBottomBarWhenPushed
プロパティを操作するのではなく、タブバーコントローラのプロパティを操作してしまうミスです。これだと、遷移先画面でベースのビューがサイズ調整されないまま表示され、使いづらくなってしまいます。
気をつけましょう(自分)
forkしたOSSライブラリをCocoaPodsで扱う
CocoaPodsを使った、既存のOSSライブラリをforkしたリポジトリをプロジェクトに組み込む方法です。
前ふり
CocoaPodsやCarthageなどのパッケージ管理システムを使うと、githubなどで公開されているOSSライブラリを簡単に使うことはよくあります。これらは簡単な定義ファイルとコマンドを使うだけで、依存関係などを含めてダウンロードしてプロジェクトに組み込めるすぐれものです。
けれども、公開されているライブラリをそのまま使うことはあまりなく、実際には少しだけ変更して使いたいことはしょっちゅうあります。開発者にプルリクエストを送ってマージしてもらうのがベストですが、実際にはそこまでするのは時間/機能的な問題で難しいことの方が多いでしょう。大抵、自分のリポジトリにforkして好みの変更を加えた上で使うことになります。
本記事では、CocoaPodsを使ったときの自分なりの方法を紹介します。「もっといい方法があるよ」という方はぜひコメントください。
ちなみに今回Carthageには触れません。 インストールしたライセンスのAcknowledgementをSettings.bundleに含める方法が便利なので・・・
OSSをforkして使うときの課題
あるOSSライブラリをgithub上でforkしたあと、自分のプロジェクトに組み込んで使うことを考えます。このとき、forkしたライブラリをCocoaPodsで使うには、いくつか課題が生じます。
- forkしたライブラリなので、勝手にcocoapods.orgに登録することはできない
- CocoaPodsがcloneする識別子になるリリースタグを勝手に打つのは憚られる
〜.podspec
ファイルはできれば変えたくない(オリジナルを尊重したい)
勝手にforkしたライブラリなので、勝手にCocoaPods.orgに登録するのは絶対に避けなければいけません。それに、本家のリリースタグを無視した名前でタグを打つことは混乱のもとになります。そして、今後プルリクエストを送ることを考えると、masterブランチで〜.podspec
を変更してしまうのは考えものです。
ようは、本家に迷惑をかけずに分かりやすく管理するにはどうしたらよいか、という問題です。
どうするか
この問題に対してどうするかということで、次のような方針を考えました。
forkしたライブラリなので、cocoapods.orgに登録することはできない Podfileでローカルのリポジトリを指定するようにします。
CocoaPodsがcloneする識別子になるリリースタグを勝手に打つのは憚られる リリース専用のブランチを作り、ひと目でforkしたものと分かるタグを打つようにします。
〜.podspec
ファイルはできれば変えたくない(オリジナルを尊重したい) masterブランチはそのままで、リリース専用のブランチだけで.podspecを変更する
それぞれの詳細は次のとおりです。
具体的な方法
Podfileでローカルのリポジトリを指定
ここだけライブラリを使用する側の作業です。使用するライブラリを記述するPodfile
で、本家のURLを書く代わりにforkしたリポジトリを記述します。
target 'MyApp' do pod 'AwesomeOSS', :git => 'https://github.com/me/AwesomeOSS.git', :branch => 'local_release' end
このときbranch
キーの記述がポイントで、次で作るブランチを指定するようにしておきます。ここでは「local_release
」という名前を指定しました。
次はforkしたリポジトリ側の作業です。
リリース専用ブランチを作り独自タグを打つ
まずリリース専用のブランチとして「local_release」を作ります。そしてそのブランチを次のルールで運用します。
- 普段の変更はmasterや他のブランチで行う
〜.podspec
の変更はこのブランチの中でだけ行う- リリース時に、このリリース用ブランチに他所での修正をマージしてタグを打つ
git-flowのmasterブランチ(=リリースブランチ)に近いイメージです。プルリクエストに使える修正はどこでやってもいいですが、その中に〜.podspec
の修正を含めないよう注意することで、あとあと管理しやすくのを狙っているわけです。
ブランチ | 用途 |
---|---|
リリース専用ブランチ(local_release) | Podfileで参照するブランチ。リリース時に他ブランチをマージして、タグを打つために使う |
他のブランチ(master, etc.) | 自分の変更を入れるために使う。リリース時にlocal_releaseへマージ。ただし.podspecファイルやLICENSEなど、本家にプルリクエストを送る際に余計になるファイルは変更しないようにする |
$ git co -b local_release
ブランチを作ったら、masterや他のブランチで必要な変更を加えていきます。そして必要な変更が終わったところでリリース専用ブランチにマージします。
マージ後は、リリース専用ブランチ上のコミットに対してリリースタグを打ちます。このとき気をつけなければいけないのは、タグに使う文字の並びです。CocoaPodsのマニュアルではSemantic Versioning形式で指定するようにとありますが、もし次のような文字列を指定してもpod lib lint
コマンドで弾かれてしまい、実際にバージョン認識してくれませんでした。
ダメだったタグ |
---|
✕ foobar-v0.4.0 |
✕ foobar0.4.0 |
✕ foobar_0.4.0 |
いろいろと試してみた結果、次のようにバージョン番号の後ろに「-
(ハイフン)」を付けて、その後に任意文字列を指定するのは大丈夫でした。
0.4.0-foobar-X.Y.Z
そこで、私は次のようにしてタグをつけています。
(a) (b) (c) 0.4.0-foobar-0.1.0 # (a): オリジナルのfork元バージョン番号 # (b): 自分のリポジトリ名 # (c): 自分のルールにおけるバージョン番号
最初にfork元のバージョン番号を示して、どの時点で派生したのかを分かるようにします。次にリポジトリが分かる名前を入れておき、最後に自分の管理下で付けるバージョン番号を付ける、というルールです。こうしておくと、clone元の由来がわかりつつ、独自のcloneであることを明確にできます。さらには自分用のバージョン番号を付けることで、独自部分の変遷も分かりやすく表すことができます。
$ git tag 0.4.0-foobar-0.1.0
.podspecはリリース専用のブランチだけで変更する
上でだいたい説明してしまいましたが、本家を尊重しなければならない変更は、極力masterなどの開発ブランチでは変えないようにしておき、リリース専用ブランチの上でだけ変更するようにします。
そうしておけば本家に敬意を表したままで、自分の変更を加えることができますし、なによりプルリクエストを作りやすくなるので、本家に取り入れてもらいやすくなるかもしれないという魂胆もあります。
作業は以上です。
まとめ
世の中に公開されているOSSライブラリに一手間加えたい場合、CocoaPodsのリポジトリURLを指定する方法が使えます。このときにブランチやタグのルールを守ることで、自分の変更も管理しやすくなる方法を紹介しました。
これよりももっといい方法があれば、ぜひコメントください。
パターンマッチング「func ~=」のデフォルト実装は「func ==」
TL;DR
パターンマッチングのルールを自分で定義するときは、~=
演算子、つまり「func ~=」を定義することになるわけですが、未定義の場合のデフォルト実装は「func ==」を使うようになっている、という話です。そして、NSObject
のデフォルトはisEqual()
が使われる、という話です。
Swiftのパターンマッチング
Swiftでオブジェクトを使ったパターンマッチングをする場合、次のように書きます。
struct A {} let a = A() let b = A() switch a { case a: print("a") // → a case b: print("b") defalut: print("default") }
このときのパターンマッチングに使われるのは、「func ~=
」なる演算子です。この演算子がBool型を返すことによってパターンマッチするかどうかが決まります。
それでは、目的の型が~=
を用意していなかった場合にはどのようになるのでしょうか。この場合、デフォルト処理が使われることになり、「func ==
」が使われます。==
の結果を、~=
の結果として利用しているのです。そして、もしユーザーやライブラリで独自の~=
を定義すると、そちらが使われます。
ケース | 使われる処理 |
---|---|
何も定義していない | デフォルト実装の~= を使う。実態は== |
~=を定義 | 定義した~= を使う |
==だけ定義 | ~= のデフォルト実装から定義した== を使う |
この振る舞いはドキュメントにもきちんと書いてあり、The Swift Programming Languageの[Language References]-[Expression Pattern]にて以下のように書かれています。
By default, the ~= operator compares two values of the same type using the == operator.
それでは実装はどうかというと、swiftソースの stdlib/public/core/Policy.swift
を見ると次のように定義されていて、ドキュメントのとおりになっているとわかります。
//===----------------------------------------------------------------------===// // Standard pattern matching forms //===----------------------------------------------------------------------===// // Equatable types can be matched in patterns by value equality. @_transparent @warn_unused_result public func ~= <T : Equatable> (a: T, b: T) -> Bool { return a == b }
以上です。
ちなみにこのstdlib/public/core/Policy.swift
、他にもいろいろな定義がありとてもおもしろいソースファイルになっています。ぜひ一度読んでみることをおすすめします。
なぜ調べてみたのか
せっかくなので、なぜこんなことを調べたかも記録に残しておきます。
NSObject
を継承したクラスでパターンマッチングをしていたところ、マッチングが行われるたびにNSObject
のisEqual()
メソッドが呼ばれていることに気づきました。
class Foo: NSObject { override func isEqual(object: AnyObject?) -> Bool { return super.isEqual(object) // ←ここにbreakpointを貼ると止まる } } let a = Foo() switch a { case a: print("a") // a default: break }
これはなぜだろう、isEqual()
と~=
とは、どのようにして繋がっているのだろうということが気になり、調べてみたというのがことの発端です。
stdlib/public/SDK/ObjectiveC/ObjectiveC.swift
の末尾で、
// NSObject implements Equatable's == as -[NSObject isEqual:] // NSObject implements Hashable's hashValue() as -[NSObject hash] // FIXME: what about NSObjectProtocol? extension NSObject : Equatable, Hashable { : @warn_unused_result public func == (lhs: NSObject, rhs: NSObject) -> Bool { return lhs.isEqual(rhs) } :
となっていて、==
→ isEqual()
と呼ばれていました。