CAAnimationでiOSアニメーション実装への恐怖を克服した話
本記事では、iOSにおけるCA Animationを活用して、滑らかなアニメーションを実装する方法を解説します。
iOS開発を始めたばかりの頃、デザイナーから「この画面にアニメーションを入れてほしい」と頼まれるたびに、私はとても緊張していました。
当時の私は、アニメーションのデザインを考えるのは簡単だと思っていました。しかし、それを実際にコードで実装するのは、まったく別の話でした。
実装にはGoogleやStackOverflow、そして同僚に助けを求めていました。その過程でアニメーションに対する恐怖心が芽生え、いつも避けるようにしていたのです。しかし、ある日を境にすべてが変わりました。
CAAnimationとの出会い
あるとき、ビュー内の一連の画像をアニメーションさせる必要がありました。最初に何をしたか?もちろんStackOverflowです!
最初のリンクで以下のコードを見つけました。
let image_1 = UIImage(named: "image-1")!
let image_2 = UIImage(named: "image-2")!
let image_3 = UIImage(named: "image-3")!
let images = [image_1, image_2, image_3]
let animatedImage = UIImage.animatedImage(with: images, duration: 2.0)
imageView.image = animatedImage
一見シンプルに見えますよね?本当にこれだけで済むなら、この記事を書くことはなかったでしょう。
実際に必要だったアニメーションはこちらです:
お分かりの通り、私のコードは目標からほど遠いものでした。完全に行き詰まってしまいました。あれほど多くのカスタマイズを加えたアニメーションを、どうやってすべて同期させればいいのでしょうか?
そこで同僚が「CAAnimationを試してみたら?」とアドバイスしてくれました。調べてサンプルプロジェクトで試してみたところ、驚くほど強力で使いやすいことがわかったのです。
Core Animationとは?
Core Animationを使うと、CPU使用率ほぼゼロで複数のアニメーションを実行できます。高いフレームレートを維持しながら、わずかなコードで多彩なカスタマイズが可能です。
詳細についてはAppleの公式ドキュメントをご覧ください:
https://developer.apple.com/documentation/quartzcore
私は数時間で基本的な実装を完成させることができました:
func addAnimation(firstImageView: UIImageView, secondImageView: UIImageView) {
let basicAnimation1 = getBasicAnimation(withInitialPostion: centerPosition, finalPos: finalPosition)
firstImageView.layer.add(basicAnimation1, forKey: "position")
let basicAnimation2 = self.getBasicAnimation(withInitialPostion: self.initalPosition, finalPos: self.centerPosition)
secondImageView.layer.add(basicAnimation2, forKey: "position")
self.addNextImage(forImageView: firstImageView)
}
func getBasicAnimation(withInitialPostion initialPos: CGPoint, finalPos: CGPoint) -> CABasicAnimation {
let basicAnimation = CABasicAnimation(keyPath: "position")
basicAnimation.fromValue = NSValue(cgPoint: initialPos)
basicAnimation.toValue = NSValue(cgPoint: finalPos)
basicAnimation.duration = 1
basicAnimation.isRemovedOnCompletion = false
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
return basicAnimation
}
この実装ではCABasicAnimationを使用しました。
CABasicAnimationクラスは、レイヤーのプロパティ(背景色、不透明度、位置、スケールなど)を2つの値の間でアニメーションさせることができます。開発者が指定するのは開始値と終了値だけ。あとはフレームワークが自動的に処理してくれます。アニメーションは次のrun loopですぐに開始されます。
さて、元の課題に戻りましょう
この実装では、2つのUIImageViewを用意し、それぞれに別々の画像を設定しました。そしてCAAnimationを使って、画像を順番にアニメーションさせていきます。
ソースコードはこちらから確認できます。
しかし、先ほどのGIFをよく見ると、何かおかしいことに気づくでしょう。ギフトボックスの画像が画面外に出きる前に、ヘッドフォンの画像が一瞬点滅してから上へ移動しているのです。
なぜこのようなことが起きるのでしょうか?
理由は、UIImageViewにアニメーションを追加すると同時に、次の画像をそのビューに設定してしまっているためです(5行目と6行目):
private func addAnimation(firstImageView: UIImageView, secondImageView: UIImageView) {
let basicAnimation1 = getBasicAnimation(withInitialPostion: centerPosition, finalPos: finalPosition)
firstImageView.layer.add(basicAnimation1, forKey: "position")
let basicAnimation2 = self.getBasicAnimation(withInitialPostion: self.initalPosition, finalPos: self.centerPosition)
secondImageView.layer.add(basicAnimation2, forKey: "position")
self.addNextImage(forImageView: firstImageView)
}
ここでの課題は、2つの画像のアニメーションをどう同期させるかという点です。しかし、CAAnimationには必ず解決策があります。
CATransactionでアニメーションを同期する
CATransactionを使うと、複数のアニメーションをまとめて同期できます。バンドルされたすべてのアニメーションが同時に開始されることが保証されるのです。
さらに、完了ブロック(completion block)を指定することもでき、バンドル内のすべてのアニメーションが完了した時点で処理を実行できます。
private func addAnimation(firstImageView: UIImageView, secondImageView: UIImageView) {
CATransaction.begin()
CATransaction.setCompletionBlock {
self.addNextImage(forImageView: firstImageView)
}
let basicAnimation1 = getBasicAnimation(withInitialPostion: centerPosition, finalPos: finalPosition)
firstImageView.layer.add(basicAnimation1, forKey: "position")
CATransaction.commit()
let basicAnimation2 = self.getBasicAnimation(withInitialPostion: self.initalPosition, finalPos: self.centerPosition)
secondImageView.layer.add(basicAnimation2, forKey: "position")
}
CATransaction.begin()でトランザクションを開始し、同期したいアニメーションをすべて記述します。最後にCATransaction.commit()を呼び出すと、ブロック内のアニメーションが一斉に開始されます。
アニメーションがどのように変わったか見てみましょう:
最後に残っていたのは、アニメーションにSpring(バネ)効果を追加することでした。幸い、CAAnimationにはこれにも解決策が用意されていました。
CASpringAnimationでバネ効果を追加
CASpringAnimationをレイヤーに追加すると、バネのような効果が得られます。まるでターゲットに向かってバネで引っ張られているように見えます。
レイヤーがターゲットから離れているほど、ターゲットに向かう加速度は大きくなります。
バネの減衰(damping)や剛性(stiffness)といった物理ベースの属性を細かく制御できます。 – Appleドキュメントより
詳しくはAppleの公式ドキュメントをご覧ください:
https://developer.apple.com/documentation/quartzcore/caspringanimation
それでは、既存のコードに組み込んでみましょう:
private func getSpringAnimation(withInitialPostion initialPos: CGPoint, finalPos: CGPoint) -> CASpringAnimation {
let basicAnimation = CASpringAnimation(keyPath: "position")
basicAnimation.fromValue = NSValue(cgPoint: initialPos)
basicAnimation.toValue = NSValue(cgPoint: finalPos)
basicAnimation.duration = basicAnimation.settlingDuration
basicAnimation.damping = 14
basicAnimation.initialVelocity = 5
basicAnimation.isRemovedOnCompletion = false
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
return basicAnimation
}
これで私の仕事は完了です。
最後に、CA Animationを使用するメリットをまとめます:
- 使いやすく、実装もシンプル
- カスタマイズの選択肢が豊富
- 複数のアニメーションを簡単に同期できる
- CPU使用率がほぼゼロ
これらはほんの一部にすぎません。CA Animationの可能性は無限大です。
今では、アニメーションの要件が来ても、自信を持ってデザインと実装ができるようになりました。この記事を読んだあなたも、同じ気持ちになれたなら嬉しく思います。
ご意見やフィードバックがあれば、ぜひお聞かせください。
-
Stuxnet(スタックスネット)とは?世界初のサイバー兵器の概要と仕組みを解説
Stuxnetとは? Stuxnet(スタックスネット)は、イランの核関連施設への攻撃を目的として設計・展開されたコンピュータワームです。物理インフラに実際的な影響を与えた、世界初の本格的なサイバー兵器とされており、イランの核濃縮用遠心分離機を標的として、重要な軍事能力に損害を与え、イランの核開発計画に大きな混乱をもたらしました。 Stuxnetの開発者が誰なのか公式には明らかにされていませんが、米国とイスラエル両政府が共同で開発したものだと広く認識されています。イランの核兵器開発計画の進展に対する懸念が高まる中、両政府は原子力研究開発センターへの空爆を含むさまざまな選択肢を検討していました。
-
Windows 10のアニメーションを無効化して動作を速く感じる方法
Windows 10 は、デスクトップに数多くのデザイン面での洗練をもたらしました。特に Microsoft の新しい Fluent Design System の導入以降、ユーザーインターフェース全体でモーションやアニメーションが格段に増えています。 Windows 10 のアニメーションを無効にする手順 Windows 10 のアニメーションは、洗練されて滑らかな操作感を演出してくれる一方で、一つの欠点があります。それは「速度」です。スタートメニューを開くたびに、タイルがアニメーションで表示されるのを待たなければなりません。新しいアプリを起動するときや、タスクバーへ最小化するときも同様です