iOS 13 で UIView の beginAnimations 等のアニメーション関連のメソッドが deprecated
タイトルの通り、iOS 13 で以下のメソッドが depecated になった。
UIView - UIKit | Apple Developer Documentation
Animating Views
class func beginAnimations(String?, context: UnsafeMutableRawPointer?) class func commitAnimations() class func setAnimationStart(Date) class func setAnimationsEnabled(Bool) class func setAnimationDelegate(Any?) class func setAnimationWillStart(Selector?) class func setAnimationDidStop(Selector?) class func setAnimationDuration(TimeInterval) class func setAnimationDelay(TimeInterval) class func setAnimationCurve(UIView.AnimationCurve) class func setAnimationRepeatCount(Float) class func setAnimationRepeatAutoreverses(Bool) class func setAnimationBeginsFromCurrentState(Bool) class func setAnimationTransition(UIView.AnimationTransition, for: UIView, cache: Bool)
代替が無くて困るという訳ではないけど既存コードの置き換えが必要になってくる。
beginAnimations, commitAnimations の置き換え
従来は begin
と commit
の間に処理を書けば良いだけだったのが、新パターン*1では Xcode での警告 Use the block-based animation API instead
の通りブロックベースのAPIを使う必要があり、また animated
が true
と false
の時で呼び出すメソッドも変わってくるため一度処理をブロックでまとめてそれぞれのメソッドの引数に渡す必要がある。
func setShowsSomething(_ shows: Bool, animated: Bool) { let block = { // do something } if animated { UIView.animate(withDuration: CATransaction.animationDuration(), animations: block) } else { UIView.performWithoutAnimation(block) } }
また、別のアニメーション処理からこのメソッドが呼び出された際にも正しく指定した時間でアニメーション処理を行って欲しい場合は overrideInheritedDuration: UIView.AnimationOptions
オプションを使うと良い。
func setShowsSomething(_ shows: Bool, animated: Bool) { let block = { // do something } if animated { UIView.animate(withDuration: CATransaction.animationDuration(), delay: 0, options: [.overrideInheritedDuration], animations: block) } else { UIView.performWithoutAnimation(block) } }
逆にこのオプションを指定しない場合は別アニメーションで指定されたアニメーション時間が継承されてアニメーションが発生する事になる。
UIView.animate(withDuration: 1.0) { self.view1.backgroundColor = .red UIView.animate(withDuration: 0.2) { // 0.2 は無視されて1.0で動作する self.view2.backgroundColor = .red } }
継承と上書きのどちらが良いかはケースバイケースだろうけど継承されるのがデフォルトの挙動というのは一応気にした方がいいと思う。
Duration に 0 を指定すればどうか?
animated
が false
の時はアニメーション時間を0にすれば挙動は同じでは?と思うかも知れない(この実装例はまま見る)。
この場合条件分岐は三項演算子で animated ? CATransaction.animationDuration() : 0
とシンプルに書けるしブロックの変数化も不要で直接記述出来る。
func setShowsSomething(_ shows: Bool, animated: Bool) { UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0, animations: { // do something }) }
1つ気を付けなければならないのはアニメーション時間の継承の問題で、例えば上記のメソッドを以下の様に呼んだとすると*2 animated: false
にも関わらず実際には1.0秒のアニメーションが発生してしまう。
UIView.animate(withDuration: 1.0) { setShowsSomething(true, animated: false) // 1.0秒でアニメーション動作 }
上記の挙動を避けるには animated
false
の際には performWithoutAnimation
を使うか、 withDuration: 0
を指定する場合は以下の様にやはり overrideInheritedDuration: UIView.AnimationOptions
オプションを使って実装する必要がある。
UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0, delay: 0, options: [.overrideInheritedDuration], animations: { // do something })
余談 performWithoutAnimation と animate:withDuration: 0 の挙動の違い
performWithoutAnimation
animate:withDuration: 0
は挙動が違う点があって、ブロック処理内でさらに呼び出された他の処理のアニメーションが有効か否かが変わってくる。
performWithoutAnimation
が間に挟まるとそれ以下のネストではアニメーションが発生しなくなる。
アニメーションしない
UIView.animate(withDuration: 1.0) { UIView.performWithoutAnimation { UIView.animate(withDuration: 0.2) { // without animation } } }
1.0秒でアニメーション
UIView.animate(withDuration: 1.0) { UIView.animate(withDuration: 0) { UIView.animate(withDuration: 0.2) { // animate with 1.0 sec } } }
performWithoutAnimation
を挟むと overrideInheritedDuration
があろうともアニメーションしない
UIView.animate(withDuration: 1.0) { UIView.performWithoutAnimation { UIView.animate(withDuration: 0.2, delay: 0, options: [.overrideInheritedDuration], animations: { // without animation }) } }
0.2秒でアニメーション
UIView.animate(withDuration: 1.0) { UIView.animate(withDuration: 0) { UIView.animate(withDuration: 0.2, delay: 0, options: [.overrideInheritedDuration], animations: { // animate with 0.2 sec }) } }
余談 performWithoutAnimation と animate:withDuration: 0 のパフォーマンスの違い
以下のコードでそれぞれを10,000回呼び出してみたところおおよそ以下の様な結果に。
やはりアニメーション無し指定の方がパフォーマンスが良いが現実的なケースでは無いのであくまで参考程度に。
iOS 13, iPhone XS | iOS 13, iPad Pro 11インチ | |
---|---|---|
performWithoutAnimation | 0.06-0.07sec | 0.064-0.074sec |
animate:withDuration: 0 | 2.74sec | 2.74sec |
@IBAction func test1() { let start = CFAbsoluteTimeGetCurrent() let view = self.view for _ in 0..<10000 { UIView.performWithoutAnimation { view?.alpha = CGFloat.random(in: 0..<1) } } let end = CFAbsoluteTimeGetCurrent() print(#function, end - start) } @IBAction func test2() { let start = CFAbsoluteTimeGetCurrent() let view = self.view for _ in 0..<10000 { UIView.animate(withDuration: 0) { view?.alpha = CGFloat.random(in: 0..<1) } } let end = CFAbsoluteTimeGetCurrent() print(#function, end - start) }
結論
アニメーション無しの際にネスト以下もアニメーションを無しにするのであれば animated
false
では performWithoutAnimation
を使う。
ネストとパフォーマンスをケアする必要が無い場合は三項演算子+ブロックの直接記述が楽。
overrideInheritedDuration
オプションはケースバイケースで。
最後に duration について
アニメーションのデフォルト値の取得には CATransaction.animationDuration()
を使ったがこのメソッドの実際の返り値は 0.25 になる。
しかし UIView.beginAnimations()
で処理を行った際に UIView.inheritedAnimationDuration
でデフォルトのアニメーション時間を確認してみるとこちらでは 0.2 が取得されるので*3、厳密に挙動を揃えたいのであれば CATransaction.animationDuration()
は使わずに 0.2 を指定する必要がある。
print(CATransaction.animationDuration()) // 0.25 UIView.beginAnimations(nil, context: nil) print(UIView.inheritedAnimationDuration) // 0.2 UIView.commitAnimations()