Core Animation周りをタイプセーフに扱えるようにする

# はじめに

UIKitでサポートされていないアニメーションをしたい場合、Core Animationを使うことになると思います。
例えば、以下のように四角から丸に変化するアニメーションです。

![green.gif](https://qiita-image-store.s3.amazonaws.com/0/60325/85be425a-1325-7e2b-abe2-da28ad8880a1.gif)

# CABasicAnimationでアニメーションさせる

上図のような四角から丸に変化するアニメーションを`CABasicAnimation`で実装する場合、意図したアニメーションを実現できる簡単な実装は以下のようになると思います。

```swift:
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.duration = 3.0
animation.fromValue = 0.0
animation.toValue = 50.0
animation.autoreverses = false
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
greenView.layer.add(animation, forKey: nil)
```

アニメーションを実現するためには上記の実装で問題ありません。
しかし、実装する上では２つの懸念があります。

## keyPathやfromValueなどがタイプセーフじゃない

CABasicAnimationのinitializerの引数であるkeyPathに注目します。
引数は`String型`であるため、以下のように**cornerRadius**の文字列をタイポする可能性があります。

```swift:
let animation = CABasicAnimation(keyPath: "conerRadius")
```

そして、アニメーションを実行しようとしたときに、アニメーションされないことでタイポに気づくことが多々あるかと思います。

また、fromValueに注目します。
fromValueは下図のように`Optional<Any>型`であるため、どんなものでも代入可能になっています。

<img width="845" alt="スクリーンショット 2018-12-12 2.44.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/60325/b74fc3f2-29a3-19bc-8330-a80fbd8563d0.png">

本来であれば、keyPathがcornerRadiusの場合は数値を代入するという関連性がなければならないはずです。

## keyPathやfromValueなどがタイプセーフな実装とは

上記であげたタイプセーフではない実装を、[TheAnimation](https://github.com/marty-suzuki/TheAnimation)（iOS、tvOS、macOSをサポート）というOSSを利用することで、タイプセーフに実装することができます。

CABasicAnimationと同等の機能をもった、[BasicAnimation](https://github.com/marty-suzuki/TheAnimation/blob/0.3.0/TheAnimation/Source/BasicAnimation.swift)を利用します。
initializerのkeyPathには、`AnimationKeyPaths.cornerRadius`を渡しています。
`AnimationKeyPaths.cornerRadius`はstaticな定数として定義されているため、入力にミスがある場合はコンパイルエラーとなるので、タイポする可能性がなくなります。

```swift:
let animation = BasicAnimation(keyPath: .cornerRadius)
```

また、AnimationKeyPaths.cornerRadiusは`AnimationKeyPath<CGFloat>型`となっています。
AnimationKeyPathのGeneric Argumentが`CGFloat型`となっており、下図のようにfromValueなどに型が紐付いています。
そのため、cornerRadiusのアニメーションに対してCGFloat型しか代入できない状態にできます。

<img width="918" alt="スクリーンショット 2018-12-12 2.44.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/60325/0ec2fbc4-8533-20bc-b25a-e63665beeac9.png">

このように実装できるため、2つあげた懸念点を改善することができます。
それでは、[TheAnimation](https://github.com/marty-suzuki/TheAnimation)内部の実装はどのようになっているのでしょうか。

# TheAnimationの実装

それでは、[TheAnimation](https://github.com/marty-suzuki/TheAnimation)がどのようにしてタイプセーフな実装を実現しているのかを順を追って解説します。
本投稿では、CABasicAnimationに関連する部分に絞って解説します。

## AnimationKeyPath

AnimationKeyPathは、**keyPathを保持しつつ型を紐付ける**ことができるようになっています。

まず、`protocol AnimationValueType`を定義していて、アニメーション時に利用する値の型に対して採用しています。

```swift
public protocol AnimationValueType {}
```

たとえば、CGColorやCGFloatに対して採用し、アニメーション時に利用できる状態にします。

```swift
extension CGColor: AnimationValueType {}
extension CGFloat: AnimationValueType {}
...
```

AnimationKeyPathはGeneric ArgumentにAnimationValueTypeを採用した型を持ち、内部でkeyPathを保持しています。

```swift
open class AnimationKeyPaths {
    fileprivate init() {}
}

public final class AnimationKeyPath<ValueType: AnimationValueType>: AnimationKeyPaths {
    let rawValue: String

    public init(keyPath: String) {
        self.rawValue = keyPath
    }
}
```

AnimationKeyPathがGeneric Argumentを持つため、static定数をAnimationKeyPathに定義しようとすると、`extension AnimationKeyPath where ValueType == CGFloat`のようにGeneric Argumentの型を確定させた状態にする必要があります。
そのため、`AnimationKeyPath.cornerRadius`のような呼び出しができず、`AnimationKeyPath<CGFloat>.cornerRadius`のような呼び出しをする必要があります。
スーパークラスを`AnimationKeyPaths`とし以下のようにstatic定数を定義することで、`AnimationKeyPaths.cornerRadius`という呼び出しができます。

```swift
extension AnimationKeyPaths {
    public static let backgroundColor = AnimationKeyPath<CGColor>(keyPath: "backgroundColor")
    public static let cornerRadius    = AnimationKeyPath<CGFloat>(keyPath: "cornerRadius")
    ...
}
```

つまり、`init<ValueType: AnimationValueType>(keyPath: AnimationKeyPath<ValueType>)`という実装になっていても、`.cornerRadius`のように型推論が利用できます。

[SwiftyUserDefaults](https://github.com/radex/SwiftyUserDefaults)の`DefaultsKeys`も上記のような実装になっています。

## protocol Animation

`protocol Animation`はTheAnimationのアニメーションクラスが採用しているprotocolであり、CAAnimationをラップしています。

```swift
public protocol Animation: class {
    var animation: CAAnimation { get }
    var key: String { get }
}
```

内部でCAAnimationを保持しているため、CAAnimationのpropertyをTheAnimationで公開している型に変換して公開しています。

```swift
extension Animation {
    public var timingFunction: TimingFunction? {
        set { animation.timingFunction = newValue?.rawValue }
        get { return animation.timingFunction.flatMap(TimingFunction.init) }
    }

    public var isRemovedOnCompletion: Bool {
        set { animation.isRemovedOnCompletion = newValue }
        get { return animation.isRemovedOnCompletion }
    }
    ...
}
```

このようにして、CAAnimationと同等の機能を実現しています。

## PrimitiveAnimation

PrimitiveAnimationは`protocol Animation`を採用しています。
Generic Argumentでは、CAPropertyAnimationまたはそのサブクラスの型をRawAnimation、AnimationValueTypeを採用した型をValueTypeとします。
initializerでは、`AnimationKeyPath<ValueType>`を引数としています。
RawAnimationはCAPropertyAnimationまたはそのサブクラスなのでinitializerでkeyPathを引数としています。
そして、AnimationKeyPath<ValueType>内で保持しているrawValueを渡してRawAnimationを初期化し、保持します。

```swift
public final class PrimitiveAnimation<RawAnimation: CAPropertyAnimation, ValueType: AnimationValueType>: Animation {

    public var animation: CAAnimation {
        return _animation
    }

    public let key: String
    
    let _animation: RawAnimation

    public var keyPath: AnimationKeyPath<ValueType>? {
        set { _animation.keyPath = newValue?.rawValue }
        get { return _animation.keyPath.map(AnimationKeyPath.init) }
    }

    public init(keyPath: AnimationKeyPath<ValueType>) {
        self._animation = RawAnimation(keyPath: keyPath.rawValue)
        self.key = keyPath.rawValue
    }
}
```

PrimitiveAnimationのextensionでGeneric Where Clauseを利用することで、RawAnimationのpropertyを利用できます。
例として、CAPropertyAnimationのサブクラスであるCABasicAnimationの場合は以下のようになります。

```swift
extension PrimitiveAnimation where RawAnimation == CABasicAnimation {
    public var fromValue: ValueType? {
        set { _animation.fromValue = newValue }
        get { return _animation.fromValue as? ValueType }
    }
    ...
}
```

そして、上記で定義したものは以下のように利用できます。

```swift
let animation = PrimitiveAnimation<CABasicAnimation, AnimationKeyPath<CGFloat>>(keyPath: .cornerRadius)
animation.fromValue = 50
```

## BasicAnimation

BasicAnimationは以下のように定義されています。

```swift
typealias BasicAnimation<ValueType: AnimationValueType> = PrimitiveAnimation<CABasicAnimation, ValueType>
```

つまり、BasicAnimationはクラス名ではなく、**RawAnimationがCABasicAnimationであるPrimitiveAnimationのtypealias**です。
[RxSwift](https://github.com/ReactiveX/RxSwift)の`Single`、`Maybe`や`Completable`も`PrimitiveSequence`をtypealiasとした上記のような実装になっています。

## ちなみに

TheAnimationはCAAnimationのサブクラスに以下のように対応しています。

| CAAnimation | TheAnimation |
| :-: | :-: |
| CAPropertyAnimation | PropertyAnimation |
| CABasicAnimation | BasicAnimation |
| CAKeyframeAnimation | KeyframeAnimation |
| CASpringAnimation | SpringAnimation |
| CATransition | TransitionAnimation |
| CAAnimationGroup | AnimationGroup |

# 最後に

明らかに間違っていても、実行して動作を確認するまで気づきにくいバグを、このようにしてコンパイル時に気づくことがきるようになります。
型を紐付けることでエラーに気づける実装は、いろんな場面で応用することができると思うので、是非試してみてください。

また、[TheAnimation](https://github.com/marty-suzuki/TheAnimation)の実装がベースになっていて、よりアニメーションの組み合わせを使いやすくした[Sica](https://github.com/cats-oss/Sica)も公開されています。