Swift アニメーション実装

Swift アニメーション実装

最近、ちゃんとアニメーション周りを勉強しないとダメだなぁと思っているにっきーです。
UIKitで提供されているUIView.animateはよく使うのですが、その他のアニメーション実装でいろいろ調べてみると便利だなと思ったものが幾つかあったので、ほんの一部ですがまとめてみました。

キーフレームアニメーション

キーフレーム追加で複数アニメーションを組み合わせる

UIView.animateKeyframesを使うと、複数のアニメーションを組みわせることができます。
CAAnimationGroupでも同じことができるのですが、ちょっとしたアニメーションの組み合わせであれば、UIView.animateKeyframesを使ったほうがお手軽にできてよさそうです。

例)移動しながら途中から回転してフェードアウト

UIView.animateKeyframes(withDuration: 2.0, delay: 0.0, animations: {
    UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1.0, animations: {
        self.imageView.center.y += 300.0
    })
    UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
        self.imageView.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))
    })
    UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
        self.imageView.alpha = 0.0
    })
}, completion: nil)

animateKeyframes

UIView.addKeyframeで指定している値ですが、UIView.animateKeyframeswithDurationで指定したアニメーション全体の長さに対して、相対値で指定するのがポイントです。

  • withRelativeStartTime:何秒後にアニメーションが開始されるかを0〜1の範囲で指定します。アニメーションの全体の長さが2秒の場合、0.5を指定するとアニメーション全体が開始してから1秒後にアニメーションが開始されます。
  • relativeDuration:この値も0〜1の範囲で指定します。アニメーションの全体の長さが2秒の場合、0.5を指定すると、アニメーションの長さは1秒になります。

UIView.animateのcompletionのネストを解消

また、よくあるケースとしてUIView.animateでアニメーション完了後のcompletionで次のアニメーションを実行させたいことがありますが、アニメーションの順次実行は複数になるとcompletion内のブロックがネストするので、見通しがわるくなるかと思います。
そういった場合にもUIView.animateKeyframesを使うと、もう少しコードがスッキリするかと思います。
(UIView.animateのcompletionでネストする場合とUIView.animateKeyframesを利用するのとでアニメーションの滑らかさも違いました。)

例)UIView.animateのcompletionでネスト

UIView.animate(withDuration: 1.0, delay: 0.0, animations: {
    self.imageView.center.y += 300.0
}) { _ in
UIView.animate(withDuration: 1.0, delay: 0.0, animations: {
    self.imageView.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))
}, completion: nil)
}

例)UIView.animateKeyframesでアニメーションの順次実行

UIView.animateKeyframes(withDuration: 2.0, delay: 0.0, options: [.autoreverse], animations: {

UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.5, animations: {
    self.blueView.center.y += 300.0
})

UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 1.0, animations: {
    self.blueView.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))
})

}, completion: nil)

配列による指定でスッキリ

キーフレームアニメーションは変更対象の値が1つの場合などは、Core AnimationのCAKeyframeAnimationを利用すると、配列で値を設定できるのでコードの見通しが良くなり、スッキリします。

例)CAKeyframeAnimationで配列による指定

let colorKeyframeAnimation = CAKeyframeAnimation(keyPath: "backgroundColor")

colorKeyframeAnimation.values = [UIColor.red.cgColor,
UIColor.green.cgColor,
UIColor.blue.cgColor]
colorKeyframeAnimation.keyTimes = [0, 0.5, 1.0]
colorKeyframeAnimation.duration = 2.0
view.layer.add(colorKeyframeAnimation, forKey: nil)

keyTimesは変更値を適用する時間をdurationに対する相対値で指定します。

スプリングアニメーション

よく見る弾むようなスプリングアニメーションですが、UIKitで提供されているものでもできますが、細かい指定ができませんでした。iOS9から利用できるようになったCASpringAnimationを使えば、もっと細かい指定ができます。

例)UIKit提供のスプリングアニメーション

UIView.animate(
    withDuration: 3.0,
    delay: 0.0,
    usingSpringWithDamping: 0.5,
    initialSpringVelocity: 0.5,
    animations: { self.imageView.frame.origin.y += 100.0 },
    completion: nil
)

例)CASpringAnimation

let spring = CASpringAnimation()
spring.keyPath = "position.y"
spring.fromValue = 100.0
spring.toValue = 200.0
spring.damping = 5.0
spring.mass = 2.0
spring.damping = 3.0
spring.stiffness = 100.0
spring.duration = spring.settlingDuration
imageView.layer.add(spring, forKey: nil)

springAnimation

CASpringAnimationでは以下の値が指定が可能なので、細かい調整ができます。

  • mass(質量:この値が大きいと振り子が振れる時間が長くなる)
  • stiffness(振り子の弾性力:この値が大きいとすぐに振り子の振幅が小さくなる)

なお、settlingDurationはreadonlyプロパティで、振り子の動きが終わるまでに必要な時間です。
そのため、アニメーション時間のdurationにセットしておくといいと思います。(settlingDurationをdurationにセットせず、durationがsettlingDurationより短い時間の場合は、スプリング中にアニメーションが終了していまします。)

CATransaction

Core Animationの終了を検知して何かしらの処理を行う場合に、CAAnimationDelegateで通知を受け取って処理することも可能ですが、CATransactionを利用するとsetCompletionBlockでアニメーション終了時の処理をブロックで渡せるので、ソースコードの見通しがよくなります。

例)CAAnimationDelegateを利用

let animation = CABasicAnimation(keyPath: "transform.scale")
animation.delegate = self
animation.duration = 0.3
animation.fromValue = 1.0
animation.toValue = 2.0
imageView.layer.add(animation, forKey: nil)

extension SomeAnimationViewController: CAAnimationDelegate {

    func animationDidStart(_ anim: CAAnimation) {
    //開始時の処理を記述
    }

    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        // 終了時の処理を記述
    }

}

例)CATransactionのsetCompletionBlockを利用

CATransaction.begin()
CATransaction.setCompletionBlock {
    // 終了時の処理を記述
}
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = 0.3
animation.fromValue = 1.0
animation.toValue = 2.0
imageView.layer.add(animation, forKey: nil)

CATransaction.commit()

UIViewPropertyAnimator

iOS10から使えるようになりました。
サンプルコードは以下の通りです。

例)UIViewPropertyAnimator

class SomeAnimationViewController: UIViewController {

@IBOutlet weak var imageView: UIImageView!
var animator: UIViewPropertyAnimator!

override func viewDidLoad(){
    super.viewDidLoad()

    animator = UIViewPropertyAnimator(duration: 3.0, curve: .linear){
        self.imageView.center.y += 300
    }
}

@IBAction func buttonTapped(_ sender: UIButton) {
    // ボタンタップでアニメーション開始
    animator.startAnimation()
}

特徴はアニメーションの開始、一時停止、終了が簡単にできることです。
UIViewPropertyAnimatorはたどるとUIViewAnimatingプロトコルを採用しているUIViewImplicitlyAnimatingプロトコルを採用しており、UIViewAnimatingで定義されているアニメーション開始、一時停止、終了のメソッドが定義されています。

また、アニメーションの状態(inactive、active、stopped)やアニメーションの進捗状況を0〜1で取得可能です。アニメーションの進捗状況fractionCompleteはセットすることも可能なので、他の何かの値に合わせて進捗状況をコントロールすることもできます。

self.animator?.fractionComplete = value

なお、途中でのアニメーション追加も簡単にできます。

animator.addAnimations {
    self.imageView.alpha = 0.0
}
animator.startAnimation()

もちろん、アニメーション終了時の処理も追加できます。

animator.addCompletion {_ in
    // 終了後からの変化する処理を記述
}

UIViewPropertyAnimatorはアニメーション途中でユーザーからの何らかのアクションなどに応じで変化するような動的なアニメーションが簡単に実装できそうです。

最後に

UI作成ではアニメーションで動きを入れるなどアニメーションを実装する機会は多いですが、なるべく煩雑にならないようにアニメーションの実装を考えていきたいと思います。

TAG

  • このエントリーをはてなブックマークに追加
にっきー
にっきー niki

最近、仕事に復帰し、子育てと仕事の両立に奮闘しているママエンジニアです。趣味はダイビングで、海に潜って魚や珊瑚の写真を撮るのが好きです。