UIView.animate
は引数が多く、animation
とcompletion
で2つクロージャーを引数に取り、ネストも深くなりやすいため、メソッドチェーンで書けるように改良してみました。
サンプル
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
var options: UIView.AnimationOptions? = nil
let resolvingAnimation: ResolvingAnimation?
init(resolvingAnimation: ResolvingAnimation?) {
self.resolvingAnimation = resolvingAnimation
}
func delay(_ value: TimeInterval) -> AnimationBuilder {
delay = value
return self
}
func duration(_ value: TimeInterval) -> AnimationExecutor {
return AnimationExecutor(resolvingAnimation: resolvingAnimation, duration: value, delay: delay, options: options)
}
func options(_ value: UIView.AnimationOptions) -> AnimationBuilder {
options = value
return self
}
func resolve() {
resolvingAnimation?({})
}
}
UiView.animate
のオプショナルな設定値の例として、delay
をプロパティとしています。
- 再帰的に、
completion
ハンドラを解決するため、途中までのアニメーションのブロックを(() -> Void) -> Void
で保持しています。
duration
は必須の値なので、こちらが終端でAnimationExecutor
をbuildするようなインターフェースにしました。
- 最後に
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
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()
}
}
return AnimationBuilder(resolvingAnimation: resolving)
}
}
- 弱参照では解放されてしまうため、強参照と値キャプチャを利用します。
- 最初は
resolvingAnimation
がnil
なので、単純にブロックを実行します。
アニメーション開始用のstaticメソッド
アニメーション開始用にstatic factoryを用意します。
final class ViewAnimator {
static func start() -> AnimationBuilder {
return AnimationBuilder(resolvingAnimation: nil)
}
}
サンプルソース
こちらです。