Swift で、アクセシブルでアニメータブルなお手製棒グラフ2（Core Animation, UIAccessibility）

［Xcode 10.3、Swift 5で書かれています］

前回
-
[前回は、`UIView`を棒グラフの“棒”のベースとして使用し、そこに`CALayer`を複数`sublayer`として加えることでグラフの値を表すところまでやりました。](https://qiita.com/OkonomiyakiYuki/items/8257478bbc62c87f672c)今回はこれをアニメーションさせます。

![OkonoMeBarChart-01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32738/b2259fb8-fdb1-37d8-cc59-e336a1f061e5.gif)

Core Animation
-
アニメーションは以下の3種類を連続して描画しています。

1. ブルブルと震える
2. 値が増加する
    3. 増える色のレイヤーが伸びる（`bounds`のアップデート）
    3. 伸びるレイヤーの上位のレイヤーが、同じ値分上方にずれる（`position`のアップデート）

このうち1は単独で、その後2-3は同時に描画されます。
つまり2-3の発動は、1のアニメーションの終了を待ってから行わなければばならない、ということです。

アニメーションをチェインするにはいくつか方法があるらしいんですが、タイミングやコールバックをつかったものはうまくいかず、最終的に`CATransaction`の`completionBlock`を使ったものに落ち着きました。


###CATransaction

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32738/4f62ae00-50f6-6b45-2728-681c7ce13e16.png" alt="Bar Chart: What to Animate" width="520">

```swift
/// Start the CATransaction here
CATransaction.begin()
```
まずは`CATransaction`を開始します。


```swift
/// bar and it's label are shaking before stretching
let shakeDuration = 0.4
let shakeAnimation = CAKeyframeAnimation(keyPath: "position.x")
shakeAnimation.values = [0, 10, -10, 10, -5, 5, -5, 0]
shakeAnimation.keyTimes = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]
shakeAnimation.duration = shakeDuration
shakeAnimation.isAdditive = true
```
1のブルブルから。ここは[hackingwithswift.comのコード](https://www.hackingwithswift.com/example-code/calayer/how-to-create-keyframe-animations-using-cakeyframeanimation)ほぼそのままです。横(x)だけの移動なので、`position.x`のキーパスを指定し、いい具合に揺れるように値を渡します。

### 連続アニメーション

問題はここから。アニメーションをチェインするには、先ほどの`CATransaction`内（`begin()`以降`commit()`以前)で`completionBlock`を使って、繋げたいアニメーションをネストする必要があります。

```swift
/// Setting completionBlock which includes other animations
CATransaction.setCompletionBlock { [unowned self] in
	/// Begin another transaction explicitly
	CATransaction.begin()
	
	let duration = 1.0
	
	for sublayer in sublayers {
		guard let colorStackLayer = sublayer as? ColorStackLayer else { continue }
		
		let singleUnit = self.bounds.height/CGFloat(unit*10)
		
		if colorStackLayer.tag == playerIndex {
			let fromValue = colorStackLayer.bounds.size.height
			let toValue = fromValue + singleUnit
			
			colorStackLayer.bounds.size.height = toValue
			
			let stretchAnimation = CABasicAnimation(keyPath: "bounds.size.height")
			stretchAnimation.duration = duration
			stretchAnimation.fromValue = fromValue
			/// DON'T NEED TO SET THE toValue BECAUSE THE LAYER IS ALREADY THERE
			/* stretchAnimation.toValue = toValue */
			
			colorStackLayer.add(stretchAnimation, forKey: "strechAnimation")
		}
			/// if layer's tag is larger than current subject layer,
		    /// that means it should be moved up its position with same value as a height.
		else if colorStackLayer.tag > playerIndex {
			let fromValue = colorStackLayer.position
			let toValue = CGPoint(x: colorStackLayer.position.x, y: colorStackLayer.position.y-singleUnit)
			
			colorStackLayer.position = toValue
			
			let slideAnimation = CABasicAnimation(keyPath: "position")
			slideAnimation.duration = duration
			slideAnimation.fromValue = fromValue

			colorStackLayer.add(slideAnimation, forKey: "slideAnimation")
		}
	}
	CATransaction.setCompletionBlock {
		/// Execute the completio closure if it was there
		if let handler = completion { handler() }
	}
	/// Commit the 2nd transaction
	CATransaction.commit()
}
self.layer.add(shakeAnimation, forKey: "shakeBarAnimation")
/// Commit the first transaction
CATransaction.commit()
```

ひとつめの`CATransaction`が終了すると次の`position`と`bounds.size.height`というふたつのCore Animation準拠のキーパスを使ったアニメーションが**同時に**、**同じ時間(`duration`)をかけて**発動します。そのおかげでバー全体が値に合わせて調整されているように見える、というわけです。

## 注意点

###toValueとfromValue

注意点としては、コメントにも書きましたが

colorStackLayer.bounds.size.height = toValue

```swift
colorStackLayer.position = toValue

let stretchAnimation = CABasicAnimation(keyPath: "bounds.size.height")
stretchAnimation.duration = duration
stretchAnimation.fromValue = fromValue

colorStackLayer.add(stretchAnimation, forKey: "strechAnimation")
```

この2種類のアニメーションはどちらも値の変更を維持するタイプのアニメーションです。たとえば`position`の移動であれば、そもそもの目的は`position`を変えて`layer`を移動させること（位置の更新）が主目的として先にあって、その過程のアニメーションはあくまで副次的なデコレーションにすぎません。

それをスムースに実現するためには、レイヤーツリーにアニメーションを加える**前に**、アニメーション後の値（=主目的の値）を目的のオブジェクトに渡してやる必要があります。

いまある`layer`の`bounds`を大きくするんだから、`CABasicAnimation`の`fromValue`と`toValue`にそれぞれ適切な値を渡してから、最後に`completionBlock`で`frame`をアップデートすればいいんじゃないかと直感的には考えてしまいそうですが、これをするとアニメーション終了後に一度もとあった値の状態に`layer`が瞬間的に戻ってしまい、その後ようやく本来のアップデートされた状態になるという、せっかくの苦労が台無しの事態に陥ってしまいます。

僕なんかはアニメーションをする前に`position`なり`bounds`を変えのには抵抗があったんですが、たしかにこれでうまくいっているので、「レイヤーツリーというのはアニメーションのコミット後にハンドルされるものなのだ」と今回学びました。

###anchorPoint

あとは前回`bar`をコンフィギュアした際に`anchorPoint`を`(0.5, 1.0)`に変更しましたが、`bounds`の`height`を変える際にこれがデフォルトの値`(0.5, 0.5)`だと、中心からストレッチしようとするので、上下に伸びることになります。上記のように`y`値が底に位置するように変えていたので、そこを支点にして意図通り一方向にアニメーションさせることができた、というわけです。

さてこれでアニメーションまでできました。

次はこの棒グラフをアクセシブルにします！