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

[こちらの記事](http://qiita.com/amay077/items/5a12d96ea72f5778a002)を読んで、そういえば `UIView` のアニメーションも普通に連続実行やろうとすれば `completion` のコールバック地獄だなぁと思って、コールバック地獄しない連続実行のアニメーションを実装してみました。

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

```swift: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](https://qiita-image-store.s3.amazonaws.com/0/63998/6e33f00f-3c27-62d4-7eca-62f38ab2fa83.gif)


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

```swift: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 なりいいねなりお願いします！
[モダンなイニシャライザ](https://iosdc.jp/2017/node/1322)
[ADDD（API-Design-Driven Development）](https://iosdc.jp/2017/node/1347)
[AVAudioEngine を使ったオーディオのループ再生](https://iosdc.jp/2017/node/1356)
[Swift 4.0 対応しようとしたら大変な目に遭った話](https://iosdc.jp/2017/node/1360)
[責務範囲を意識するというお話](https://iosdc.jp/2017/node/1369)
[UIView のプロパティーとメソッドたち](https://iosdc.jp/2017/node/1386)