Swift でアニメーションの連続実行をしてみる話

  • 18
    いいね
  • 6
    コメント

こちらの記事を読んで、そういえば UIView のアニメーションも普通に連続実行やろうとすれば completion のコールバック地獄だなぁと思って、コールバック地獄しない連続実行のアニメーションを実装してみました。

アプローチは非常に単純で、アニメーションブロックを逐一取り出して、アニメーション終わった後に自分を再帰呼び出ししていく API を作るだけです。結果として下記のような Playground で実行できるソースコードで書くことができます:

Playground.swift
let base = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
base.backgroundColor = .white
PlaygroundPage.current.liveView = base

let view = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
view.backgroundColor = .blue
base.addSubview(view)

// アニメーションブロック
UIView.animate(eachBlockDuration: 1, eachBlockDelay: 0, eachBlockOptions: .curveEaseInOut, animationBlocks:
    { view.frame.origin = .zero },
    { view.frame.size.width = 400 },
    { view.frame.size.height = 400 }
) { (finished) in
    print(finished)
}

これを Playground のライブビューで見るとこんな感じです:
7月-25-2017 13-06-15.gif

さて、この animate(eachBlock...) とやらの実装ですが、こんな感じになります:

UIView.swift
private extension ArraySlice {

    var startItem: Element {
        return self[self.startIndex]
    }

}

extension UIView {

    private typealias `Self` = UIView

    private static func animate(eachBlockDuration duration: TimeInterval, eachBlockDelay delay: TimeInterval, eachBlockOptions options: UIViewAnimationOptions, animationArraySlice: ArraySlice<() -> Void>, completion: ((_ finished: Bool) -> Void)?) {

        let animation = animationArraySlice.startItem

        UIView.animate(withDuration: duration, delay: delay, options: options, animations: animation) { (finished) in

            let remainedAnimations = animationArraySlice.dropFirst()

            if remainedAnimations.isEmpty {
                completion?(finished)

            } else {
                Self.animate(eachBlockDuration: duration, eachBlockDelay: delay, eachBlockOptions: options, animationArraySlice: remainedAnimations, completion: completion)
            }

        }

    }

    public static func animate(eachBlockDuration duration: TimeInterval, eachBlockDelay delay: TimeInterval = 0, eachBlockOptions options: UIViewAnimationOptions = .curveEaseInOut, animationBlocks: (() -> Void)..., completion: ((_ finished: Bool) -> Void)? = nil) {

        let isFinished = animationBlocks.isEmpty

        guard isFinished == false else {
            completion?(isFinished)
            return
        }

        let animationArraySlice = ArraySlice(animationBlocks)

        Self.animate(eachBlockDuration: duration, eachBlockDelay: delay, eachBlockOptions: options, animationArraySlice: animationArraySlice, completion: completion)

    }

}

非常に似てる二つのメソッド、一つは public で一つは private ですが、なぜ二つあるのかというと可変長パラメーターをそのまま別の関数に渡すことができないし、実行効率的にもプログラム内部の使いやすさ的にも Array より ArraySlice の方が上(再生成する手間もなければ .dropFirst() の結果をそのまま使える)ので、引数を ArraySlice<() -> Void> 型にしてるメソッドを用意しているが、でも ArraySlice の場合は使う側としては可変長引数と比べればやっぱ微妙に軍配が下なので、使う側には可変長引数、内部には ArraySlice、というふうに使い分けます。また、public 側で引数の長さが必ず 1 以上であることも保証してあげれば first? というオプショナルのアンラップも必要なくなります。最後は private のメソッドを ArraySlice が空になるまで再帰呼び出してやれば、渡されたアニメーションブロックを逐一に実行することができます。便利ズイ₍₍(ง˘ω˘)ว⁾⁾ズイ

p.s. 宣伝:今年こそ!と思って iOSDC のトークいっぱい応募しました!興味あるトークあればぜひ RT なりいいねなりお願いします!
モダンなイニシャライザ
ADDD(API-Design-Driven Development)
AVAudioEngine を使ったオーディオのループ再生
Swift 4.0 対応しようとしたら大変な目に遭った話
責務範囲を意識するというお話
UIView のプロパティーとメソッドたち