こちらの記事を読んで、そういえば UIView
のアニメーションも普通に連続実行やろうとすれば completion
のコールバック地獄だなぁと思って、コールバック地獄しない連続実行のアニメーションを実装してみました。
アプローチは非常に単純で、アニメーションブロックを逐一取り出して、アニメーション終わった後に自分を再帰呼び出ししていく API を作るだけです。結果として下記のような Playground で実行できるソースコードで書くことができます:
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 のライブビューで見るとこんな感じです:
さて、この animate(eachBlock...)
とやらの実装ですが、こんな感じになります:
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 のプロパティーとメソッドたち