しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

UIView.animateをメソッドチェーンで書く

UIView.animateは引数が多く、animationcompletionで2つクロージャーを引数に取り、ネストも深くなりやすいため、メソッドチェーンで書けるように改良してみました。

サンプル

f:id:scior:20190430230201g:plain

ViewAnimator.start()
    .delay(1.0)
    .duration(0.8)
    .animate {
        self.subview.transform = self.subview.transform.translatedBy(x: 0, y: 200.0)
    }
    .duration(0.8)
    .animate {
        self.subview.transform = self.subview.transform.translatedBy(x: 200.0, y: 0)
    }
    .duration(0.8)
    .animate {
        self.subview.transform = self.subview.transform.translatedBy(x: 0, y: -200.0)
    }
    .duration(0.8)
    .animate {
        self.subview.transform = self.subview.transform.translatedBy(x: -200.0, y: 0)
    }
    .resolve()

実装

難しいことはせず、Builderパターンと再帰を利用しています。

AnimationBuilder

アニメーションのパラメーターを拾うBuilderです。

typealias AnimationHandler = () -> Void
typealias ResolvingAnimation = (@escaping AnimationHandler) -> Void

final class AnimationBuilder {
    var delay: TimeInterval? = nil // (1)
    var options: UIView.AnimationOptions? = nil // (1)
    let resolvingAnimation: ResolvingAnimation? // (2)

    init(resolvingAnimation: ResolvingAnimation?) {
        self.resolvingAnimation = resolvingAnimation
    }

    func delay(_ value: TimeInterval) -> AnimationBuilder {
        delay = value
        return self
    }
    func duration(_ value: TimeInterval) -> AnimationExecutor { // (3)
        return AnimationExecutor(resolvingAnimation: resolvingAnimation, duration: value, delay: delay, options: options)
    }
    func options(_ value: UIView.AnimationOptions) -> AnimationBuilder {
        options = value
        return self
    }
    func resolve() { // (4)
        resolvingAnimation?({})
    }
}
  1. UiView.animateのオプショナルな設定値の例として、delayをプロパティとしています。
  2. 再帰的に、completionハンドラを解決するため、途中までのアニメーションのブロックを(() -> Void) -> Voidで保持しています。
  3. durationは必須の値なので、こちらが終端でAnimationExecutorをbuildするようなインターフェースにしました。
  4. 最後にresolveを呼ぶとアニメーションを実行します。

AnimationExecutor

メソッドチェーンを実現するため、Builderのdurationの直後のみanimate出来るようにしています。

final class AnimationExecutor {
    let resolvingAnimation: ResolvingAnimation?
    let delay: TimeInterval?
    let duration: TimeInterval
    var options: UIView.AnimationOptions? = nil

    init(resolvingAnimation: ResolvingAnimation?, duration: TimeInterval, delay: TimeInterval? = nil, options: UIView.AnimationOptions?) {
        self.resolvingAnimation = resolvingAnimation
        self.duration = duration
        self.delay = delay
        self.options = options
    }

    func animate(handler: @escaping AnimationHandler) -> AnimationBuilder {
        let resolving: (@escaping AnimationHandler) -> Void = { [duration, delay, options] completion in // (1)
            let animation = { UIView.animate(
                withDuration: duration,
                delay: delay ?? TimeInterval.zero,
                options: options ?? [],
                animations: handler,
                completion: { _ in completion() }
            )}

            if let resolvingAnimation = self.resolvingAnimation {
                resolvingAnimation(animation)
            } else {
                animation() // (2)
            }
        }
        return AnimationBuilder(resolvingAnimation: resolving)
    }
}
  1. 弱参照では解放されてしまうため、強参照と値キャプチャを利用します。
  2. 最初はresolvingAnimationnilなので、単純にブロックを実行します。

アニメーション開始用のstaticメソッド

アニメーション開始用にstatic factoryを用意します。

final class ViewAnimator {
    static func start() -> AnimationBuilder {
        return AnimationBuilder(resolvingAnimation: nil)
    }
}

サンプルソース

こちらです。