ObjecTips

Swift & Objective-C で iOS とか macOS とか

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 の置き換え

従来は begincommit の間に処理を書けば良いだけだったのが、新パターン*1では Xcode での警告 Use the block-based animation API instead の通りブロックベースのAPIを使う必要があり、また animatedtruefalse の時で呼び出すメソッドも変わってくるため一度処理をブロックでまとめてそれぞれのメソッドの引数に渡す必要がある。

    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 を指定すればどうか?

animatedfalse の時はアニメーション時間を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()

*1:とは言えiOS 4.0以降のメソッドなので全く新しくは無い

*2:こんな意味不明なコード自体書くなという話は置いといて、メソッドの利用者が自分の書いたコードをどう使うかまでは制御出来ないのでメソッド側でどこまでケアすべきかという観点で、、と長々と釈明

*3:何年か前にデフォルトアニメーションの時間を調べた時に0.25だった気がするけどデフォルト値が変わったのか記憶違いなのか