Core Animationでframe情報が更新されなくて困ったので対応方法を模索しました
iOSアプリで、アニメーションで開閉するボタンを実装したところ、Core Animationの動作で苦しめられました。
そこで調べたこと、得られた知見を書いていきます。
サンプル
説明のために、サンプルアプリを作成しました。
このようなアニメーションをするボタンです。
そもそもiOS開発でアニメーションを実装するには
今回僕はiOSのアニメーション実装がはじめてだったので、そもそもどうしたらいいかから調べはじめました。
大方針としては3つあります。
-
UIView
のanimate()
を使う - Core Animationを使う
-
UIViewPropertyAnimator
を使う
僕は最初2を選択しました。
理由は1と2しか知らなくて、要件を満たすアニメーションをするためには2しかないと思ったからです。
今見直してみると、animate()
でもなんかできたようにも見えますが、どうでしょうね。
animate()
については、Appleの公式見解だとあんま使って欲しくなさそうな雰囲気を感じます。
Apple discourages using these methods. Use the UIViewPropertyAnimator class to perform animations instead.
難易度的には1<3<2だと思います。
Core Animationの操作はレイヤー操作になるので癖があって難しいですが、
Appleのドキュメントによるとビッドマップ画像の操作なので、パフォーマンスがいいとのことです。
詳しくわかってませんが、UIViewの操作だとアニメーションの計算でヘビーな演算をしないといけないところを、
Core Animationだと単に画面の1ptを何色に変更する、みたいな計算だけですむので計算の負担が軽くて効率的、みたいなのかなと推測しています。
Core Animationの実装
Core Animation(CABasicAnimation
)で実装した例がこちらです。
private func addAnimation() {
// layer.position = CGPoint(x: 0, y: frame.minY)
// layer.anchorPoint = CGPoint(x: 0, y: 0)
let animation = CABasicAnimation(keyPath: #keyPath(bounds))
animation.beginTime = CACurrentMediaTime() + 3.0
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
animation.fromValue = bounds
var toValue = bounds
toValue.size.height = 10
animation.toValue = toValue
animation.isRemovedOnCompletion = false
animation.fillMode = CAMediaTimingFillMode.forwards
layer.add(animation, forKey: nil)
// DelegateやCATransactionだと別画面に遷移した後のViewで不整合が出たので、タイマー処理にした
Timer.scheduledTimer(withTimeInterval: 4.0, repeats: false) { [weak self] _ in
self?.setTitle(nil, for: .normal)
self?.completion()
}
}
簡単に解説すると、アニメーションの開始前の状態(fromValue)と開始後の状態(toValue)を設定してやります。
それをUIView
が持っているCALayer
にadd()
します。
やろうと思えば、もっと複雑なアニメーションを設定することもできます。
アニメーションはデフォルト設定だと、終了後にアニメ開始前の状態に戻るようになっているので、
animation.isRemovedOnCompletion = false
とanimation.fillMode = CAMediaTimingFillMode.forwards
を指定しています。
アニメーションの状態として、boundsを指定していますが、frameは指定できません。
ここにも無理だと書いてありますね。
コメントアウトしてる、position
とかanchorPoint
というのは、CALayer
の制約で、
もし上記の高さの縮小する方向をコントロールしたいのであれば、正しく指定してやる必要があります。
サンプルだと上手く指定できなかったのでやめました。
(全然直感的に動いてくれません)
全体を見たい方はGistにあげたので、そちらで見てください。
Core Animationで実装したときの問題点
この実装だと、一つ大きな問題が出てきます。
それはアニメーション後にタップ範囲が変わらないことです。
↑タップするとボタンの背景色が変わるようにしました。
アニメーション終了すると、広告ボタンは消えているのに、タップ範囲が残ってしまいました。
このサンプルだと色が変わる(高さを小さくしたので目立たない)だけですが、実際はタップすると別画面への遷移が発生することになりました。
なぜこんなことが起きるか
なぜこんなことが起きるのかというと、Core AnimationはCALayer
を操作して、Viewは操作しないためです。
(ここから)
↑この図にある通り、UIViewの世界とCALayerの世界は処理的には完全分離されていて、
ディスプレイに最終的に映る画面にはレイヤー層の処理が加えられて出力する、ということになっています。
つまり、Core Animationで操作する状態変数は、あくまでUIViewのframe情報とは別モノなので、上手いこと開発者が調整する必要があります。
このレイヤー処理はViewの細かいプロパティ情報を知らなくても、複雑なアニメーションを軽量な計算で動かせるので、
iPhoneのなめらかなアニメーションの実現に一役買っていたと思われます。
画像をアニメーションして終わり、という要件ならこれでいいんですけど、
今回実現したかったのがボタンのアニメーションだったのでめちゃくちゃ困りました。
対処法はあるにはある
一応Core Animationでも頑張れば要件を実現できます。
アニメーション終了後をDelegateでとるか、CATransactionでトランザクション化するとCompletionが指定できるので、
ここで辻褄を合わせれば、なんとかなります。
タップイベントの範囲がおかしいなら、アニメーション終了後に自分のframeを更新する、という手があります。
ただ僕のケースだと、frame更新するとUIButtonのレイアウトが自動調整されて、意図していたデザインが実現できなかったので、厳しかったです。
あとサンプルアプリではなく、実際に僕がやってたアプリはタブバーで別ページに遷移できるアプリで、
Core Animationは別ページ遷移すると、遷移したタイミングでcompletionを即時実行して、アニメーションのDelayは残ります。
苦肉の策でTimerにしています。
UIViewPropertyAnimator
ならframeが更新できる
血を吐きながら実装したんですが、その後「ホントにこんなやり方しかないん?」と思って調べていると、
UIViewPropertyAnimator
というiOS 10から追加されたクラスを発見しました。
こちらの実装例が、こちら。
/// ボタンが閉じるアニメーションを付与する
private func addAnimation() {
let animator = UIViewPropertyAnimator(duration: 1.0, curve: .easeOut) { [weak self] in
guard let self = self else { return }
self.frame.origin.y = self.frame.maxY - 50.0
}
animator.addCompletion { [weak self] _ in
self?.setTitle(nil, for: .normal)
self?.completion()
}
animator.startAnimation(afterDelay: 3.0)
}
Core Animationで消耗した点がほとんどなくなりました。。。
ソース全体が見たい方は↓を参照ください。
Core Animationは難しい
コードの記述量の差からもわかるとおり、Core Animationを使うとコード的に難解になるのを実感しました。
あまりに難解なので、サンプルコード書いてる途中でこんな動作が発生しました。
frame更新できない問題よりも、コードが難解になる問題もCore Animationはキツいですね。
リファレンスと一言
-
iOSアプリ開発でアニメーションするなら押さえておきたい基礎
- 最初に参照した神Qiita記事です。簡潔なコードと対応する動作が載っていて、素晴らしい記事
- ただ2016年の記事なので、
UIViewPropertyAnimator
が書かれていない
-
Core Animation Programming Guide
- Core Animationの全てが詰まった公式ドキュメント(アーカイブ)
-
【iOS】 Core Animationまとめ
- これも良Qiita記事
- ↑の公式ドキュメントが英語というのと内容的にも難しめなので、適宜こちらを参照した
-
[iOS10] UIViewPropertyAnimatorによるアニメーション
- Core Animationじゃない方法を模索する中で、
UIViewPropertyAnimator
という存在を知り、それで検索したら発見したクラスメソッドの良記事 - キーワードを知ってることの重要性を改めて認識
- Core Animationじゃない方法を模索する中で、
Discussion