Standby Tune

Android,iOSのアプリ開発について発見した事、感じた事をまとめます

Swiftで簡単にレイヤード・アニメーションを定義できるライブラリ”Anima”を作りました

そろそろ世界的なiOS Application Engineerの祭典「WWDC」が間近ですね!
僕は今回始めてのWWDC and 海外となるので、非常に楽しみかつ緊張しております。英語通じるだろうか・・・。
現地や世界中のエンジニアからいろいろ開発事情などのお話を聞いて回りたいです。

前置きはこの程度にして、本題です。
今回私はSwift3/iOS9以降用ライブラリである Anima というライブラリを作成しました。

github.com

リリース直後に Github Trend 入りし、おかげさまでStarは300を超えました!🎉

Example

Animaは非常に簡単にCALayerのアニメーションを記述することができます。

f:id:SatoshiN21:20170526032345g:plain

例えば、上のような連続的、かつグルーピングされたアニメーションを記述する場合、以下のように記述することができます。

// "■" のグルーピングされたアニメーションを定義
let startAnimations: [AnimaType] = [.moveByY(-50), .rotateByZDegree(90)]
let moveAnimations: [AnimaType] = [.moveByX(50), .rotateByZDegree(90)]
let endAnimations: [AnimaType] = [.moveByY(-50), .rotateByZDegree(90)]

// "■"のアニメーションを実行。各アニメーションの終了時にUILabelのフェードインアニメーションを開始する
animaView.layer.anima
    .then(.opacity(1.0))
    .then(group: startAnimations)
    .then(group: moveAnimations, options: labelAnimaOption(index: 0))
    .then(group: moveAnimations, options: labelAnimaOption(index: 1))
    .then(group: moveAnimations, options: labelAnimaOption(index: 2))
    .then(group: moveAnimations, options: labelAnimaOption(index: 3))
    .then(group: endAnimations, options: labelAnimaOption(index: 4))
    .then(group: [.scaleBy(0.0), AnimaType.opacity(0.0)])

// 各"■"のアニメーションが終わった際にUILabelのアニメーションを実行するAnimaOptionを取得
func labelAnimaOption(index: Int) -> [AnimaOption] {
    let labelAnima = labels[index]?.layer.anima

    return [.completion({
        labelAnima?.then(.opacity(1)).fire()
    })]
}

CAAnimationクラスを用いてアニメーションする場合、各アニメーションに対してCAAnimation, CAGroupAnimationでアニメーションを定義し、CAAnimationDelegateやCATransactionを用いて終了時の処理を・・・と途方もないステップ数を踏まないと実装ができないので、それと比較してもかなり簡潔にアニメーションを記述できるかと思います!

Features

Animaは主に以下の機能を備えています。

  • 連続的アニメーション
  • グルーピングアニメーション
  • 連続的アニメーションの一時停止・再生
  • 移動(移動量指定/座標指定)、透明度、回転(x,y,z, 弧度法・度数法) 拡大縮小、その他CALayerのAnimatableなプロパティのアニメーション
  • タイプセーフなAnimatable propertyのKeyPath指定・及び実行
  • 各種オプション
    • duration
    • timingFunction ( bounce系を覗いた easings.net のほぼ全てのアニメーションとスプリングアニメーションを指定可能 )
    • autoreverse
    • repeat(count: Float)
      • リピート回数
      • .infinity指定可能
    • completion
      • 完了通知

移動アニメーション ( moveByX )

単純な移動アニメーションであれば以下のようにワンラインで記述することができます。

layer.anima.then(.moveByX(50)).fire()

f:id:SatoshiN21:20170526032655g:plain

基本的な動作はCALayerから取得するAnimaオブジェクトを起点とします。 Anima.then(_ animationType: AnimaType, options: [AnimaOption] = []) -> Selfに指定の AnimaTypeを指定します。

AnimaType.moveByX(CGFloat)は指定した移動量分現在の位置からX座標を動かします。もし、指定した座標に動かす場合はAnimaType.moveTo(x: CGFloat, y: CGFloat)を定義してください。

Animaは座標移動系のアニメーションを実行する際、内部的にはCALayer.positionを動かさず、CATransformで動かしています。これは、Viewに紐付いているレイヤー、いわゆるLayer Based ViewのCALayerのpositionを更新した際、Viewの干渉により意図せずCALayer.positionがリセットされてしまうためです。

最終的にAnima.fire()を実行したタイミングでアニメーションが実行されます!

連続的アニメーション

Anima.then()はSelfを返却する為、チェーン状にアニメーションを記述することができます。

// 未指定時のアニメーション時間を指定(second)
Anima.defaultDuration = 1

// X軸に50(1sec),Y軸に100アニメーション(3sec)する
layer.then(.moveByX(50))
     .then(.moveByY(100), options: [.duration(3)])
         .fire()

各アニメーション毎に個別のオプションを割り当てる事ができます!

連続的アニメーションの遅延実行・一時停止・再開

連続的アニメーションは遅延実行が可能です。例えば上記の例を元に.moveByXを行った後3秒停止、その後moveByYを実行したい場合はAnima.then(waitFor t: TimeInterval)を指定します。

// X軸に50(1sec),3秒停止、Y軸に100アニメーション(3sec)する
layer.then(.moveByX(50))
        .then(waitFor: 3)
        .then(.moveByY(100), options: [.duration(3)])
        .fire()

また、ユーザの操作によりアニメーションを一時停止、再開する場合はそれぞれAnima.pause()Anima.resume()が用意されています。

private var anima: Anima?

override func viewDidLoad() {
    super.viewDidLoad()

    // 永遠に回転するアニメーションを定義
    let anima = animaView.layer.anima
    anima
        .then(.rotateByZDegree(360), options: [.repeat(count: .infinity)]).fire()
    // メンバ変数に保持
    self.anima = anima
}

@IBAction func buttonClicked(_ sender: UIButton) {
    // ボタンクリック時にAnima.statusを見て一時停止・再開する
    switch anima?.status {
    case .paused?, .willPaused?:
        anima?.resume()
    case .active?:
        anima?.pause()
    default:
        return
    }
}

Animaは性質上Anima.pause()を呼び出した際、直ちにアニメーションが停止する訳ではなく、チェーン状に記述した各アニメーションがそれぞれ実行完了になった際に次のアニメーションを実行することなく一時停止をする仕様となっています。

Animaはstatusというプロパティを持っており、ここでアニメーションの実行状況が把握可能です。 各caseの状態は以下のとおりです。

  • notFired = fire()呼び出し前
  • active = アニメーション中
  • willPaused = pause()を呼び出したが、まだ実行中のアニメーションが停止していない状態
  • paused = 一時停止中
  • completed = 全てのアニメーションが実行終了した状態

グルーピング・アニメーション

最初にお見せしたアニメーションのように、回転しつつ移動するなどのアニメーションも指定可能です。 同じタイミングでアニメーションを実行する場合、Anima.then(group: [AnimaType] options: ~)を指定します。

let animations: [AnimaType] = [.moveByY(50), .rotateByZDegree(90)]

layer.anima
     .then(group: animations)
       .fire()

独自アニメーション

AnimaはCALayerで用意されているanimatableなpropertyであればほぼ全て用意されている為、上記で紹介したAnimaType以外にもopacityやborderColor, READMEで紹介しているanchorPointやshadow, borderColorなどをアニメーションさせることができます。 ※何が用意されているかは AnimaTypeのソースを見てもらえれば確認できます

ただし、このままではCALayerのanimatable propertyしかアニメーションができません。 独自propertyをAnimaでアニメーション可能にする為に、AnimaType.original(keyPath: String, from: Any?, to: Any)を用意しています。

例えばCALayerのサブクラスであるCAEmitterLayerのanimatable propertyであるemitterPositionをアニメーションする場合は以下のようになります。

let layer = CAEmitterLayer()
layer.emitterPosition = CGPoint(x: 100.0, y:100.0)

layer.anima
    .then(.original(keyPath: #keyPath(CAEmitterLayer.emitterPosition), from: layer.emitterPosition, to: CGPoint(x: 200.0, y:200.0)))
    .fire()       

keyPathにanimatable propertyを指定し、開始値と終了値を指定すれば、独自のpropertyに対しアニメーションが可能です!

まとめ

正直もうCAAnimationを書くのが辛いというのもあり、今回Animaを作りました。 上記で紹介した以外にもExampleアプリで触って動かせるサンプルを作ったので、是非とも試してください!

$ pod try Anima

PRやissue(とstar)お待ちしております🙃