Swiftでくるくる落ちるアニメーション

こんにちは増島です! 今回はiOSの話をします!

最近ではリッチなアニメーションをしてるアプリを見る機会も多くなってきましたね。 アニメーション系の実装方法を色々知っておくと便利かと思います。

iOSでアニメーションの実装方法はUIViewのメソッドやCore Animationなど色々あるかと思いますが、今回はCore Animationのいくつかのクラスを使用してタイトルの通りのアニメーションを実装してみました。

完成はこのような感じです!

完成イメージ

このアニメーションでは「CABasicAnimation」「CAAnimationGroup」「CATransform3DMakeRotation」などを使用して三つのアニメーションを同時実行しています。

  • Z方向に横回転するアニメーション
  • 上から下に移動するアニメーション
  • 最前面に表示し続けるアニメーション

Z方向に回転するアニメーション

アニメーションの定義はこの通りです。

    // 回転
    let rotateAnimation = CABasicAnimation(keyPath: "transform")
    rotateAnimation.duration = 0.3
    rotateAnimation.repeatCount = Float.infinity
    let transform = CATransform3DMakeRotation(CGFloat(M_PI),  0, 1.0, 0)
    rotateAnimation.toValue = NSValue(CATransform3D : transform)

大事なのはこの二つです。

CATransform3DMakeRotation(CGFloat(M_PI),  0, 1.0, 0)  
rotateAnimation.repeatCount = Float.infinity  

CATransform3DMakeRotationの第一引数で回転する角度をラジアンで渡すのですが、360度を渡すと0度と同じにことになり回転しなくなるので、180度にしています。またrepeatCountで何回も繰り返すようにして一回転するようなアニメーションを実現しています。
回転するときに縦を軸にするため第3引数(y軸)に1.0を渡しています。

上から下に移動するアニメーション

// 移動
let endPoint = CGPointMake(self.layer.position.x, UIScreen.mainScreen().bounds.height)  
let moveAnimation = CABasicAnimation(keyPath: "position")  
moveAnimation.duration = fallingDuration  
moveAnimation.fromValue = NSValue(CGPoint: self.layer.position)  
moveAnimation.toValue = NSValue(CGPoint: endPoint)  

これについては特筆すべきとこはあまりないです。 fromValueに開始点、toValueに終着点を指定してアニメーションオブジェクトを作成します。

最前面に表示し続けるアニメーション

このアニメーションで重要になってくるのがこの処理です。

画面上に一つのviewのみ存在し、そこにアニメーションするimageViewなどをaddSubViewする場合であれば特に問題は起きません。
しかし、通常1画面にはボタンやら何やらUIオブジェクトが沢山あり、一つのviewだけという事はあまりないと思います。
その場合、アニメーションしてるviewが他のviewの背面に隠れてしまい、思った通りの表示にならない場合があります。例えばこんな感じに!

失敗

その場合にz軸に移動するアニメーションを実行し続けることで、背面に隠れてしまう現象を回避出来ます。 実装はこんな感じです。

// zPosition(最前面に表示し続けるための処理)
let zAnimation = CABasicAnimation(keyPath: "transform.translation.z")  
zAnimation.fromValue = self.layer.bounds.size.width  
zAnimation.toValue = zAnimation.fromValue  

keyPathにtransform.translation.zを指定する事でZ軸方向に移動し続けています。

アニメーションをまとめて実行

作成したアニメーションを別々にaddしてもいいのですが、CAAnimationGroupを使ってグルーピングしたほうが一元管理出来て便利だと思います。
またシーケンシャルなアニメーションをしたい場合にCAAnimationGroupを使うと簡単に実現出来ます。

let animationGroup = CAAnimationGroup()  
animationGroup.duration = fallingDuration  
animationGroup.repeatCount = 1  
animationGroup.animations = [moveAnimation, rotateAnimation, zAnimation]  
animationGroup.delegate = self  
self.layer.addAnimation(animationGroup, forKey: "SampleAnimation")  

全体を通すとこんな実装で実現出来ます。

class ViewController: UIViewController {

  @IBAction func tappedStart(sender: AnyObject) {
    let animationImageView = starImageView
    self.view.addSubview(animationImageView)
    animationImageView.falling()
  }

  private var starImageView: FallingImageView {
    let image = UIImage(named: "icon_star")!
    let unitSize = image.size
    let xPoint =  CGFloat(arc4random() % UInt32(self.view.frame.size.width - unitSize.width))
    let rect = CGRect(x: xPoint, y: -unitSize.height, width: unitSize.width, height: unitSize.width)
    let imageView = FallingImageView(frame: rect)
    imageView.image = image
    return imageView
  }

}

// くるくるアニメーションするUIImageView
class FallingImageView: UIImageView {  
  private var fallingDuration: NSTimeInterval = 2.5
  private let animationKey = "fallingAnimation"

  func falling(delayDouble: Double = 0.1) {
    // 回転
    let rotateAnimation = CABasicAnimation(keyPath: "transform")
    rotateAnimation.duration = 0.3
    rotateAnimation.repeatCount = Float.infinity
    let transform = CATransform3DMakeRotation(CGFloat(M_PI),  0, 1.0, 0)
    rotateAnimation.toValue = NSValue(CATransform3D : transform)
    // 移動
    let endPoint = CGPointMake(self.layer.position.x, UIScreen.mainScreen().bounds.height)
    let moveAnimation = CABasicAnimation(keyPath: "position")
    moveAnimation.duration = fallingDuration
    moveAnimation.fromValue = NSValue(CGPoint: self.layer.position)
    moveAnimation.toValue = NSValue(CGPoint: endPoint)
    // zPosition(最前面に表示し続けるための処理)
    let zAnimation = CABasicAnimation(keyPath: "transform.translation.z")
    zAnimation.fromValue = self.layer.bounds.size.width
    zAnimation.repeatCount = Float.infinity
    zAnimation.toValue = zAnimation.fromValue

    let animationGroup = CAAnimationGroup()
    animationGroup.duration = fallingDuration
    animationGroup.repeatCount = 1
    animationGroup.animations = [moveAnimation, rotateAnimation, zAnimation]
    animationGroup.delegate = self

    self.layer.addAnimation(animationGroup, forKey: self.animationKey)
  }

  override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
    if flag {
      self.removeFromSuperview()
    }
  }
}

簡単にアニメーションをするならUIIViewのanimateWithDurationで十分かもしれませんが、ちょっと複雑なことをするならCore Animationは非常に便利だと思いました!!