Swift3でインスタンスから型メソッドを呼ぶ

ある型(class/struct)のインスタンスを使って型メソッドを呼ぶには、type(of:)関数を使います。

class Foo {
    static var bar: Int { return 100 }
}

let foo = Foo()
foo.bar // error: static member 'bar' cannot be used on instance of type 'Foo'
type(of: foo).bar  // 100

上のコンパイルエラーになっている箇所のようにfoo.barとは呼べないので注意。あと、Swift2で使えていたdynamicTypeプロパティはSwift3では使えないので、こちらも注意です。

SKView&SKSceneを画面の回転に対応させる

iOSバイスを回転させたときに、SpriteKitで使うSKViewSKSceneをどうやって・どのタイミングで追従させるかについてです。

SpriteKitの中で完結させようとなると、SKSceneが持つdidChangeSize(CGSize)メソッドを使うべきなのでしょう。しかしこのメソッドはサブクラスでオーバーライドしてあげないと使えず、デリゲートで外から制御させることができません。回転のためだけにオレオレスーパーSKSceneを作るのもなんだかなあと調べていたら、UIViewControllerviewWillTransition(to:with:)を使う方法がStackOverflowで紹介されていて便利だったのでこちらでも紹介します。

stackoverflow.com

Swiftで書くとしたら次のようになります。

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    // SKViewの調整 (※AutoLayout設定済みなら不要)
    skView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
    // SKSceneのサイズをSKViewに合わせる
    currentScene.size = size

    // 新しい画面サイズで修正が必要な処理の実行
}

viewWillTransition(to:with:)もサブクラスでオーバーライドして使うものとはいえ、ビューコントローラはたいていサブクラスで運用しているので、ずっと気軽に使えます。

ジャンプするSKActionを作る

SKActionには「ジャンプする」を表すアクションがなかったので作ってみました。

public extension SKAction {
    class func jump(height: CGFloat, duration seconds: TimeInterval) -> SKAction {
        var initialY: CGFloat = 0.0
        var initialFlag = true

        let customAction = SKAction.customAction(withDuration: seconds) {
            node, elapsedTime in
            if initialFlag == true {
                initialFlag = false
                initialY = node.position.y
            }

            let t = elapsedTime/CGFloat(seconds)

            let y: CGFloat
            if t > 0.9995 {
                y = 0
            } else {
                y = -4 * height * t * (t - 1)
            }
            node.position.y = initialY + y
        }
        return customAction
    }
}

パラメータに「ジャンプの高さ」と「滞空時間」を設定すると、その場でジャンプしてくれます。

let jump = SKAction.jump(height: 100, duration: 1.0) // 100ptの高さで1.0秒ジャンプする
node.run(jump)

理屈は簡単で、滞空時間を0-1で正規化した後、(0,0),(ジャンプの高さ,1),(1,0)の3点を通る2次方程式に当てはめているだけです。 ただしそれだけだと、cocos-2dのジャンプでよくあった「ジャンプを続けるとだんだん位置がずれていく」という、最終位置の誤差が積み重なる問題が出てしまうので、ある程度の時間が立ったら地面におろしてしまうようにしています。

ループ途中で抜ける処理をfilter()等で書き換える

Swiftを触っていると、色々なループをmap/forEachなどで置き換えたくなります。その1パターンの実装例を考えてみます。

目的のデータを探すためのループ

ループ処理のユースケースとして、下例のような「目的の値を取り出すために見つかるまでループ。見つかったら値を保持してbreak」という使い方があります。

let array = Array(0..<1000)

var ret = 0
for i in array {
    if i == 33 {
        ret = i
        break
    }
}

これを何も考えずfilter()で置き換えた場合のコードは次のとおりです。

// filterで目的の値を見つける
let ret1 = array.filter{ $0 == 33 }.first

シンプルですしこれでも良いように思いますが、実はこれだとパフォーマンスの問題を抱えてしまいます。Playgroundなどで実行回数を調べてみると分かりますが、filterは配列全体を一旦スキャンするため、要素数によっては大量の繰り返し処理が紛れ込むことになってしまうからです。

// filterで目的の値を見つける
let ret1 = array.filter{ $0 == 33 }.first // 1001times !

配列のサイズが数万だった場合には目も当てられません。無駄な処理を避けて、見つかったところで繰り返しを抜けるにはどうしたらいいでしょうか。

lazy

この場合、lazy を配列に付けてあげます。すると配列の値がオンデマンドで参照されるようになり、繰り返しが必要なところまでで止まります。

// lazy化した配列を使い、filterで目的の値を見つける
let ret1 = array.lazy.filter{ $0 == 33 }.first // 35times

上記のソースではfirstが得られた時点、つまりfiltertrueを返した時点で処理が終わり、それ以降は計算が行われません(34でなく35になっているのは、lazy)。 余分な処理を実行しなくてもすむようになりました。

他の方法があればぜひコメントなどで教えてください🙇

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側のコードは以下のとおり。こちらのサイトにあるコードを使わせてもらっています。

github.ioなどを使って公開すると簡単にhttpsで公開できるので楽チンです。

<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セットにして、それを何回も繰り返すという処理です。

参考サイト

coderwall.com

SpriteKitでテクスチャアトラスからSKSpriteNodeを作る

SpriteKitでノード(SKNode)に画像を使いたい場合、SKSpriteNodeをテクスチャアトラスから作るのが便利です。

通常、テクスチャアトラスを作るにはたいてい専用のツールが必要で、隙間なく上手に並べたいときなどなかなか面倒だったりします。ですがXcodeだと.xcassetsファイルがその機能を自動的に実現してくれています。

使い方は簡単です。

ファイル準備

  1. .xcassetsファイルを選択
  2. アセット一覧が表示されているペインの左下部にあるプラス()ボタンをクリックして「New Sprite Atlas」を選択。テクスチャアトラスを表すフォルダ(下の画像だと右側に4つの□が並んだフォルダ)が作成される f:id:swiftlife:20160414123840p:plain
  3. あとは通常の画像と同じように、テクスチャアトラスに入れたい画像をそのフォルダに入れるだけ

f:id:swiftlife:20160414124007p:plain これで、ビルドした時に自動でテクスチャアトラスを作成し、他のアセットも含んだ.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)

細かい設定はできませんが、とても簡単に用意できるのは便利ですね。