UIViewPropertyAnimatorを使いこなそう

本記事は、[FOLIOアドベントカレンダー](https://adventar.org/calendars/3424)の7日目の記事になります。

# UIViewPropertyAnimatorとは

UIViewPropertyAnimatorはユーザーの操作とアニメーションを連動させたり、アニメーションの開始・停止・反転などのコントロールを可能にするAPIです。

なので、UIViewPropertyAnimatorを使うとインタラクティブなアニメーションを簡単につくることができます。（お馴染みの`UIView.animate`だけでは出来なかったことですし、LayerやCoreAnimationを使えば可能ではありましたが個人的に複雑な実装になる印象があります。）

また、iOS10から導入されたAPIなのでUIViewPropertyAnimatorをご存知の方は多いのではないでしょうか。

# なぜ今UIViewPropertyAnimatorなのか

まず、iOS10で導入されたAPIであることから運用しているアプリのサポートOSとの兼ね合いを考えても、このAPIを使えるアプリも昨今では多くなっているのではないかと思います。

そして、WWDC2018で[Designing Fluid Interfaces](https://developer.apple.com/videos/play/wwdc2018/803/)が注目されました。

このFluid Interfacesを実現するための一要素として、インタラクティブアニメーションの理解は必要だと思い、UIViewPropertyAnimatorにフォーカスをあてました。


# UIViewPropertyAnimatorを使った簡単なサンプル
[サンプルコード1]:https://qiita.com/items/bc2e1fa0c3bbd0a51d17#uiviewpropertyanimator%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E7%B0%A1%E5%8D%98%E3%81%AA%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB

分かりやすい例として、UIViewPropertyAnimatorを使うと下記のようにユーザーのジェスチャ操作と連動してViewの伸縮やblurをかけるようなアニメーションが簡単に作れます。


![blursample 2018-12-05 11_52_35](https://user-images.githubusercontent.com/1317847/49487123-79f0dd00-f884-11e8-96e0-dc19dd8a6bd8.gif)


まずはUIViewPropertyAnimatorのざっくりした使い方を見てもらうため、このサンプルコードの全容を下記に載せます。
アニメーション操作の箇所にはコメントをつけています。


```swift

class ViewController: UIViewController {

    enum ImageViewState: Int {
        case open
        case close

        var opposite: ImageViewState {
            switch self {
            case .open:
                return .close
            case .close:
                return .open
            }
        }
    }

    private lazy var imageView: UIImageView = {
        let imageView = UIImageView()
        imageView.image = #imageLiteral(resourceName: "background")
        imageView.isUserInteractionEnabled = true
        return imageView
    }()

    private let blurView: UIVisualEffectView = {
        let blurView = UIVisualEffectView()
        blurView.isUserInteractionEnabled = false
        return blurView
    }()
    private var imageViewTopConstraint = NSLayoutConstraint()
    private var offset: CGFloat {
        return view.frame.maxY - 80
    }

    // UIViewPropertyAnimatorの生成
    private var animator = UIViewPropertyAnimator(duration: 3.0, curve: .easeInOut)
    private var animationProgress: CGFloat = 0.0

    private var imageViewState: ImageViewState = .close

    override func viewDidLoad() {
        super.viewDidLoad()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: view.topAnchor)
        view.addSubview(imageView)

        NSLayoutConstraint.activate(
            [
                imageViewTopConstraint,
                imageView.leftAnchor.constraint(equalTo: view.leftAnchor),
                imageView.rightAnchor.constraint(equalTo: view.rightAnchor),
                imageView.heightAnchor.constraint(equalTo: view.heightAnchor)
            ]
        )

        blurView.translatesAutoresizingMaskIntoConstraints = false
        imageView.addSubview(blurView)
        NSLayoutConstraint.activate(
            [
                blurView.topAnchor.constraint(equalTo: imageView.topAnchor),
                blurView.leftAnchor.constraint(equalTo: imageView.leftAnchor),
                blurView.rightAnchor.constraint(equalTo: imageView.rightAnchor),
                blurView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor)
            ]
        )

        let recognizer = UIPanGestureRecognizer()
        recognizer.addTarget(self, action: #selector(self.imageViewPanned(recognizer:)))
        imageView.addGestureRecognizer(recognizer)
    }

    private func setupAnimator(state: ImageViewState) {
        // 逆再生をoff
        animator.isReversed = false

        // アニメーション実行中の場合はアニメーションを設定しない
        if animator.isRunning {
            return
        }
        // アニメーションの追加
        animator.addAnimations {
            switch state {
            case .close:
                self.imageViewTopConstraint.constant = -self.offset
            case .open:
                self.imageViewTopConstraint.constant = 0
            }
            self.view.layoutIfNeeded()
        }
        // アニメーションの追加
        animator.addAnimations {
            switch state {
            case .close:
                self.blurView.effect = UIBlurEffect(style: .light)
            case .open:
                self.blurView.effect = nil
            }
        }
　      // アニメーション完了時の処理
        animator.addCompletion { position in
            switch position {
            case .start:
                self.imageViewState = state
            case .end:
                self.imageViewState = state.opposite
            case .current: ()
            }

            switch self.imageViewState {
            case .close:
                self.imageViewTopConstraint.constant = 0.0
            case .open:
                self.imageViewTopConstraint.constant = -self.offset
            }
        }
    }

    @objc private func imageViewPanned(recognizer: UIPanGestureRecognizer) {
        switch recognizer.state {
        case .began:
            // Animatorの設定
            setupAnimator(state: imageViewState)
            // アニメーションの一時停止
            animator.pauseAnimation()
            // アニメーションの進捗率を保持
            animationProgress = animator.fractionComplete
        case .changed:
            let translation = recognizer.translation(in: imageView)
            var fraction = -translation.y / offset
            if imageViewState == .open { fraction *= -1 }
            // ユーザー操作とアニメーションの進捗率を連動させインタラクティブアニメーションを実現するキーポイント
            animator.fractionComplete = fraction + animationProgress

        case .ended:
            let yVelocity = recognizer.velocity(in: imageView).y
            if yVelocity == 0 {
                // アニメーション再開
                animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
            }

            switch imageViewState {
            case .close:
                if yVelocity > 0 {
                    // 逆再生モードON
                    animator.isReversed = true
                }
            case .open:
                if yVelocity < 0 {
                    // 逆再生モードON
                    animator.isReversed = true
                }
            }
            // アニメーション再開
            animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)

        default: ()
        }
    }

}
```

各プロパティやメソッドは後ほど説明していきますが、何をやっているのか簡易な説明をすると

* UIViewPropertyAnimatorのインスタンス生成（以後Animatorと記載）
* Animatorにアニメーションや終了時の処理を設定
* アニメーションの開始・一時停止
* ユーザーのジェスチャ操作とアニメーションの連動

といったことをしています。

手っ取り早くUIViewPropertyAnimatorでインタラクティブアニメーションのやり方を見たい方は[アニメーションの進捗率を操作する（インタラクティブアニメーション）][fractionComplete] を先に見ると良いと思います。


それでは、UIViewPropertyAnimatorの説明と使ってみて得た知見を順番に紹介していこうと思います。

# UIViewPropertyAnimatorを紐解く

## UIViewPropertyAnimatorの状態

UIViewPropertyAnimatorを使う上で、状態を理解することはとても重要になります。

### UIViewAnimatingState

[UIViewAnimatingState]:https://qiita.com/items/bc2e1fa0c3bbd0a51d17#uiviewanimatingstate

UIViewPropertyAnimatorは3つの状態を持ちます。
その状態と遷移図が下記になります。

![uiviewpropertyanimatorstate.png](https://qiita-image-store.s3.amazonaws.com/0/27785/91a135ef-96c3-4698-8740-5909e7c727a0.png)

[AppleDocumentより](https://developer.apple.com/documentation/uikit/uiviewanimating)

この状態は、コード上で`UIViewAnimatingState`というenumで定義されています。

```swift
public enum UIViewAnimatingState : Int {

    
    case inactive // The animation is not executing.

    case active // The animation is executing.

    case stopped // The animation has been stopped and has not transitioned to inactive.
}
```

|状態|説明|
|:--|:--|
|Inactive|アニメーションが実行されていない状態（Animatorの初期状態）|
|Active|アニメーションが実行中もしくは一時停止の状態|
|Stopped|アニメーションが停止している状態|


Animatorの状態遷移は

* `inactive`状態のAnimatorに、`アニメーション開始`もしくは`アニメーション一時停止`の操作で`active`に遷移します。

* `active`状態のAnimatorのアニメーションが正常に完了すると`inactive`に遷移します。

* `active`状態のAnimatorに、`アニメーション停止`の操作で`stopped`状態に遷移します。
 
* `stopped`状態のAnimatorに`終了`の操作で`inactive`状態に遷移します。

になります。

**現在の状態から図の矢印が出ている方向にしか状態は遷移できません。**

イレギュラーな状態遷移をした場合はエラーとなりアプリがクラッシュします。

今後の説明でもAnimatorの状態はよく出てきますので覚えておくと捗ります。

## UIViewPropertyAnimatorのインスタンスを生成する

UIViewPropertyAnimatorのイニシャライザを紹介します。
イニシャライザで生成されたAnimatorの状態は全て`inactive`です。

### `init(duration:curve:animations:)`

```swift
public convenience init(duration: TimeInterval, curve: UIView.AnimationCurve, animations: (() -> Void)? = nil)
````

|引数|説明|
|:--|:--|
|duration|アニメーションの時間を指定|
|curve|アニメーションカーブ(`easeInOut`, `easeIn`, `easeOut`, `linear`)を指定|
|animations|アニメーションブロック|

アニメーションの時間、アニメーションカーブ(`easeInOut`, `easeIn`, `easeOut`, `linear`)、アニメーションブロックを書いてAnimatorを生成します。

```swift
let animator = UIViewPropertyAnimator(duration: 1.0, curve: .easeIn) {
    //アニメーションコード
    self.targetView.center.x += 100
}
```

### `init(duration:controlPoint1:controlPoint2:animations:)`

```swift
public convenience init(duration: TimeInterval, controlPoint1 point1: CGPoint, controlPoint2 point2: CGPoint, animations: (() -> Void)? = nil)
``` 

|引数|説明|
|:--|:--|
|duration|アニメーションの時間を指定|
|point1|ベジェ曲線の最初の制御点を指定|
|point2|ベジェ曲線の２つ目の制御点を指定|
|animations|アニメーションブロック|

ベジェ曲線を指定して独自のアニメーションカーブを持つAnimatorを生成します。

ベジェ曲線の調整は[cubic-bezier.com](http://cubic-bezier.com/)を使うと捗ります。

```swift
let animator = UIViewPropertyAnimator(duration: 1.0, controlPoint1: CGPoint(x: 0.58, y: 0.11), controlPoint2: CGPoint(x: 0.98, y: 0.54)) {
    //アニメーションコード
    self.targetView.center.x += 100
}
```
### `init(duration:dampingRatio:animations:)`

```swift
public convenience init(duration: TimeInterval, dampingRatio ratio: CGFloat, animations: (() -> Void)? = nil)
```

|引数|説明|
|:--|:--|
|duration|アニメーションの時間を指定|
|ratio|バネ効果の調整 (0.0 〜 1.0)|
|animations|アニメーションブロック|

バネ効果を持つAnimatorを生成します。

```swift
let animator = UIViewPropertyAnimator(duration: 1.0, dampingRatio: 0.7) {
    //アニメーションコード
    self.targetView.center.x += 100
}
```

### `init(duration:timingParameters:)`

```swift
public init(duration: TimeInterval, timingParameters parameters: UITimingCurveProvider)
```

|引数|説明|
|:--|:--|
|duration|アニメーションの時間を指定|
|parameters|UITimingCurveProviderプロトコルに準拠したオブジェクトを指定|

バネ効果の詳細な設定もしくは、アニメーショカーブの詳細な設定を持つAnimatorを生成します。

* `UITimingCurveProvider`プロトコルに準拠しているクラスは、
  * バネ効果の詳細な設定ができる`UISpringTimingParameters`クラス
  * アニメーショカーブの詳細な設定ができる`UICubicTimingParameters`クラス
  
があります。

## Animatorの状態を調べる
### `state`プロパティ

```swift
public var state: UIViewAnimatingState { get }
```

[先の図][UIViewAnimatingState]で説明したAnimatorの状態は`state`プロパティで確認することができます。

```swift
let animator = UIViewPropertyAnimator(duration: 1.0, curve: .easeIn) {
    //アニメーションコード
    self.targetView.center.x += 100
}
animator.state // .inactive
```


## アニメーションを開始する

UIViewPropertyAnimatorはイニシャライザでAnimatorを生成しただけではアニメーションは実行されません。
Animatorに設定されたアニメーションを現在地から開始するためのメソッドを紹介します。

また、`inactive`状態のAnimatorのアニメーションを開始するとAnimatorの状態は`active`に遷移します。

### `startAnimation()`メソッド

```swift
public func startAnimation()
```

アニメーションが開始されます。

```swift
let animator = UIViewPropertyAnimator(duration: 3, curve: .easeIn) {
    self.blueView.center.x += 200
}
// startAnimation() を呼ぶことで発火する
animator.startAnimation()
```

![Dec-01-2018 17-34-18.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/b02ae596-d5f7-da10-ca22-bed6bb534210.gif)


### `startAnimation(afterDelay:)`メソッド
```swift
public func startAnimation(afterDelay delay: TimeInterval)
```

|引数|説明|
|:--|:--|
|delay|アニメーションを何秒後に開始するか指定|

`delay`で指定した値秒後にアニメーションを開始させます。


### `runningPropertyAnimator(withDuration:delay:options:animations:completion:)`クラスメソッド

```swift
open class func runningPropertyAnimator(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions = [], animations: @escaping () -> Void, completion: ((UIViewAnimatingPosition) -> Void)? = nil) -> Self
```

|引数|説明|
|:--|:--|
|duration|アニメーションの時間を指定|
|delay|アニメーションを何秒後に開始するか指定|
|options|AnimationOptionsを指定|
|animations|アニメーションブロック|
|completion|アニメーション終了ブロック|

お馴染みの`UIView.animate`のようにスグにアニメーションを発火させたい場合は、このクラスメソッドを使うことで実現できます。

`options`には`UIView.AnimationOptions`を複数指定できますが、アニメーションの方向やトランジション系のOptionは無視されます。(`repeat`, `autoreverse`,`transitionXXX`など)

余談ですが、`transitionXXX`系の効果を使いたい場合は、`animations`ブロックに従来の`UIView.transition`を記述することで可能になります。

`completion`ブロックにはアニメーション終了後の処理を書きます。
詳しくは[addCompletion(_:)メソッド][addCompletion(_:)]で説明しているので参照してください。

```swift
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 3.0, delay: 0.0, options: [.curveEaseIn], animations: {
    self.blueView.center.x += 200
}) { positon in
    // アニメーション終了後の処理
    switch position {
    case .end: ()
    case .start: ()
    case .current: ()
    }
}
```

![Dec-01-2018 17-34-18.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/b02ae596-d5f7-da10-ca22-bed6bb534210.gif)

また、返り値として`UIViewPropertyAnimator`のインスタンスが返ってきます。

もちろん、返り値から取得したAnimatorは状態を持っているので、既に実行されているアニメーションに様々な操作をすることが可能です。
これが`UIView.animate`のようにアニメーションを実行したら終了するまで関与できないものとは大きく違うところです。

## アニメーションが実行中か調べる


### `isRunning`プロパティ

[isRunning]:https://qiita.com/items/bc2e1fa0c3bbd0a51d17#%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%8C%E5%AE%9F%E8%A1%8C%E4%B8%AD%E3%81%8B%E8%AA%BF%E3%81%B9%E3%82%8B

```swift
public var isRunning: Bool { get }
```

アニメーションが実行中かを返します。

```swift
let animator = UIViewPropertyAnimator(duration: 3, curve: .easeIn) {
    self.blueView.center.x += 200
}
animator.startAnimation()
animator.isRunning // true
```


## アニメーションを追加する

[addAnimation]:https://qiita.com/items/bc2e1fa0c3bbd0a51d17#%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B

Animatorにアニメーションを追加することができます。
アニメーションの追加はAnimatorが`inactive`でも`active`状態でも可能です。
Animatorが`stopped`状態の時にアニメーションを追加しようとするとクラッシュします。
アニメーションの追加は複数可能です。

### `addAnimations(_:)`メソッド

```swift
optional public func addAnimations(_ animation: @escaping () -> Void)
```

|引数|説明|
|:--|:--|
|animation|追加するアニメーションブロック|

```swift
let animator = UIViewPropertyAnimator(duration: 3, curve: .easeIn) {
    self.blueView.center.x += 200
}
// アニメーションの追加
animator.addAnimations {
    self.blueView.alpha = 0.0
}
animator.startAnimation()
```

![Dec-01-2018 21-51-50.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/4382e843-64a1-d15b-5f1b-8db4f80e4338.gif)

`active`状態で実行中のアニメーションのAnimatorに新たにアニメーションを追加した場合は即時、追加したアニメーションが実行されます。 

### `addAnimations(_:delayFactor:)`メソッド

```swift
open func addAnimations(_ animation: @escaping () -> Void, delayFactor: CGFloat)
```

|引数|説明|
|:--|:--|
|animation|追加するアニメーションブロック|
|delayFactor|アニメーションの残りの時間に対する割合(0.0〜1.0)の間で指定|

* `delayFactor`は`0.0`〜`1.0`の間で入力します。これはアニメーションの**残り時間**に対して、そのアニメーションの開始をどれだけ遅延させるかという割合を指定します。

  * 例えば`3.0`秒のアニメーションに対して、`delayFactor`を`0.5`で指定した場合、そのアニメーションの開始タイミングは `残り時間 × delayFactor`で求められます。

  * すなわち `3.0 * 0.5`で`1.5`秒後に`addAnimations(_:delayFactor:)`メソッドのアニメーションブロックが実行されます。

下記のアニメーションのように`1.5`秒後に`blueView`の`cornerRadius`が変化し始めることが分かります。

```swift
let animator = UIViewPropertyAnimator(duration: 3, curve: .easeIn) {
    self.blueView.center.x += 200
}
// 残り時間に対して0.5の割合後に角丸にするアニメーション
animator.addAnimations({
    self.blueView.layer.cornerRadius = 50.0
}, delayFactor: 0.5)
animator.startAnimation()
```

![Dec-01-2018 22-20-35.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/92672655-87dc-44af-0739-3ecf11d38e48.gif)

`delayFactor`はアニメーションの**残り時間**に対して割合を指定することから、既に実行中のアニメーションを持つAnimatorに対しても`delayFactor`を指定したアニメーションを追加できます。

```swift
let animator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 3.0, delay: 0.0, options: [.curveEaseIn], animations: {
    self.blueView.center.x += 200
})
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
    animator.addAnimations({
        self.blueView.layer.cornerRadius = 50.0
    }, delayFactor: 0.5)
}
```

![Dec-01-2018 22-34-12.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/2661e8c2-d85c-5b6a-d566-218fc67a49a8.gif)

## アニメーション終了後の処理を追加する

アニメーションが終了した時に実行する処理をAnimatorに追加することができます。
Animatorに対して複数追加することができます。

### addCompletion(_:)メソッド
[addCompletion(_:)]:https://qiita.com/items/bc2e1fa0c3bbd0a51d17#addcompletion_%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

```swift
open func addCompletion(_ completion: @escaping (UIViewAnimatingPosition) -> Void)
```

|引数|説明|
|:--|:--|
|completion|アニメーション終了後の実行ブロック。<br>引数として`UIViewAnimatingPosition`が渡される|

引数として`UIViewAnimatingPosition`が渡ってきます。
`UIViewAnimatingPosition`はアニメーションの位置を示すenumで`end`,`start`,`current`の列挙子で構成されています。

```swift
public enum UIViewAnimatingPosition : Int {

    
    case end

    case start

    case current
}
```

この値に応じてアニメーション終了後の処理を書くことができます。

`end`の場合はAnimatorに設定されているアニメーションが最後の位置で終了したという意味であり、アニメーションブロックに記述した更新対象のプロパティの値は、アニメーションブロックで指定した値となっています。

下記はアニメーション終了時に`alpha`を`0.0`にする処理を持つAnimatorの挙動です。

```swift
let animator = UIViewPropertyAnimator(duration: 3, curve: .easeIn) {
    self.blueView.center.x += 200
}
animator.addAnimations({
    self.blueView.layer.cornerRadius = 50.0
}, delayFactor: 0.5)

animator.addCompletion { position in
    switch position {
    case .start: 
        // startなのでcenter.xとcornerRadiusはアニメーション開始前の値が入っている
        ()
    case .current: 
        // 途中で終わったのでその時の値がcenter.xとcornerRadiusに反映されている
        ()
    case .end:
        // endなのでcenter.xは=200された値でありcornerRadiusは`50.0`になっている
        self.blueView.alpha = 0.0
    }
}
animator.startAnimation()
```

![Dec-02-2018 17-11-54.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/4370e4f5-5a8c-b120-77d3-991ca923f700.gif)

## アニメーションを一時停止させる

Animatorの実行中のアニメーションを一時停止します。

### pauseAnimation()メソッド

```swift
public func pauseAnimation()
```

`pauseAnimation()`メソッドを使うと実行中のアニメーションを一時停止することができます。

`inactive`状態のAnimatorに`pauseAnimation()`を使うと`active`に状態遷移します。

`startAnimation()`によってアニメーション実行中かつ`active`状態のAniamtorになりますが、そのAnimatorに`pauseAnimation()`した場合、アニメーションは一時停止しますが、状態は`active`のままです。

このことから、`active`状態のAnimatorのアニメーションが実行中なのか一時停止中なのかは[isRunning][isRunning]プロパティで判定します。
アニメーションが一時停止中であれば`isRunning`の値は`false`です。

また、アニメーションが一時停止中のAnimatorに対して`startAnimation()`すると再びアニメーションが動き出します。
この時、`isRunning`は`true`になります。


```swift
animator = UIViewPropertyAnimator(duration: 3, curve: .easeIn) {
    self.blueView.center.x += 200
}
animator.startAnimation()

@IBAction func tappedPauseButton(_ sender: Any) {
    animator.pauseAnimation()
}

@IBAction func tappedStartButton(_ sender: Any) {
    animator.startAnimation()
}
```

![Dec-02-2018 22-19-04.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/63d55bfc-8755-7f0f-9467-fb070fdb0df9.gif)


## アニメーションを逆再生する

### `isReversed`プロパティ

```swift
public var isReversed: Bool { get set }
```

アニメーションが逆再生されているかどうか判定するプロパティです。
このプロパティを変更することでアニメーションの再生方向を反転することができます。

```swift

animator = UIViewPropertyAnimator(duration: 2.0, curve: .easeInOut) {
    self.blueView.center.x += 250
}

@IBAction func tappedStartButton(_ sender: Any) {
    animator.startAnimation()
}

@IBAction func tappedPauseButton(_ sender: Any) {
    animator.pauseAnimation()
}

@IBAction func tappedContinueButton(_ sender: Any) {
    animator.isReversed.toggle()
}

```

![Dec-04-2018 23-21-34.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/16e45c98-f544-960d-8bb7-3eaf5827a535.gif)

なお、`isReversed`が`true`でアニメーションが実行され、そのまま終了した場合、`completion`ブロックに渡される`position`引数の値は`start`が渡されます。
これは、アニメーションが開始されたスタートの位置でアニメーションが終了したことを表しています。
[positionについて][addCompletion(_:)]

## アニメーションを終了・破棄する

`active`状態のAnimatorに設定されているアニメーションを破棄します。
アニメーションを破棄した際にアニメーションの終了処理である`completion`ブロックを呼び出したいか否かで呼び出し方が変わります。

`inactive`状態でアニメーションが設定されているAnimatorに対して`終了・破棄`を呼んでもアニメーションは破棄されません。

### stopAnimation(_:)

```swift
public func stopAnimation(_ withoutFinishing: Bool)
```

|引数|説明|
|:--|:--|
|withoutFinishing|アニメーションを終了・破棄させた時に状態を`stopped`にするか`inactive`状態にするか指定|

`stopAnimation(_ withoutFinishing: Bool)`メソッドを使うとAnimatorの実行中のアニメーションを終了・破棄させることができます。

* `withoutFinishing`に`true`を指定した場合は、アニメーションが停止した時のAnimatorの状態が`inactive`になります。
  * この場合、アニメーションが終了した時の処理である`completion`が設定されているAnimatorでも`completion`ブロックは呼ばれません。

```swift
animator = UIViewPropertyAnimator(duration: 3, curve: .easeIn) {
    self.blueView.center.x += 200
}
animator.addCompletion { (position) in
    //そもそも呼ばれない
    self.blueView.layer.cornerRadius = 50
}

@IBAction func tappedStartButton(_ sender: Any) {
    animator.startAnimation()
}


@IBAction func tappedStopButton(_ sender: Any) {
    animator.stopAnimation(true)
    print(animator.state) // inactive
}
```

![Dec-02-2018 22-46-49.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/6a79acd8-6f2f-e65b-7228-9be0f47ab286.gif)


* `withoutFinishing`に`false`を指定した場合は、アニメーションが停止した時のAnimatorの状態は`stopped`になります。
  * `stopped`状態から`inactive`状態にするには`finishAnimation(at:)`メソッドを呼ぶ必要があります。
  * `finishAnimation(at:)`には引数に[addCompletion(_:)メソッド][addCompletion(_:)]で説明した`UIViewAnimatingPosition`を指定します。
  * これを指定することによって、アニメーションの終了位置を操作することができ、その終了位置を引数にした`completion`ブロックが呼び出され、Animatorの状態が`inactive`に遷移します。
  * `finishAnimation(at:)`はAnimatorを`stopped`から`inactive`に遷移させる時にのみ使うメソッドなので、`stopAnimation(_:)`を`false`で呼び出した時以外で使うとクラッシュします。

```swift
animator = UIViewPropertyAnimator(duration: 3, curve: .easeIn) {
    self.blueView.center.x += 200
}
animator.addCompletion { (position) in
    switch position {
    case .start:
        self.blueView.layer.cornerRadius = 10
    case .current:
        self.blueView.alpha = 0.0
    case .end:
        // ここが呼ばれる
        self.blueView.backgroundColor = .red
    }
}

@IBAction func tappedStopButton(_ sender: Any) {
    animator.stopAnimation(false)
    print(animator.state) // stopped
    animator.finishAnimation(at: .end) // アニメーションの終了位置を指定
    print(animator.state) // inactive
}
```

![Dec-02-2018 23-05-16.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/8bcb64fd-fb86-e75a-bf2b-13a8ec9c506c.gif)

このように`finishAnimation(at:)`で`end`を指定すると`blueView`の位置が設定したアニメーションの終了位置である`center.x + 200`になります。


また、`stopAnimation(_:)`を通して`inactive`状態になったAnimatorは、アニメーションが全て破棄されていますので、そのままAnimatorに対して`startAnimation()`をしても開始するアニメーションが設定されていないので何も起きません。
しかし、Animatorの状態は`active`に遷移します。

`stopAnimation(_:)`を通して`inactive`状態になったAnimatorには、[addAnimationsメソッド][addAnimation]でアニメーションを新たに設定してから`startAnimation()`をすることでアニメーションが動くようになります。

```swift
let animator = UIViewPropertyAnimator(duration: 3, curve: .easeIn) {
    self.blueView.center.x += 200
}
animator.startAnimation()
animator.stopAnimation(true)
// animator.startAnimation() 動かない

animator.addAnimations {
    self.blueView.center.x += 200
}
// これは動く
animator.startAnimation()
```

## 速度や時間などの設定を変更してアニメーションを再開する

一時停止中のアニメーションを持つAnimatorの時間や速度を変更してアニメーションを再開します。
これを使う時のAnimatorの状態は`active`かつアニメーションが一時停止中(`isRunning == false`)の場合でないとエラーになりアプリがクラッシュします。

### `continueAnimation(withTimingParameters:durationFactor:)`メソッド

[continueAnimation]:https://qiita.com/items/bc2e1fa0c3bbd0a51d17#continueanimationwithtimingparametersdurationfactor%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

```swift
open func continueAnimation(withTimingParameters parameters: UITimingCurveProvider?, durationFactor: CGFloat)
```

|引数|説明|
|:--|:--|
|parameters|UITimingCurveProviderプロトコルに準拠したオブジェクトを指定|
|durationFactor|アニメーションの残り時間に掛ける倍率の指定|

このメソッドを使うと、既にAnimatorに設定されている時間や速度や効果を変更することができます。

挙動を確認してみましょう。

```swift
animator = UIViewPropertyAnimator(duration: 2.0, curve: .easeInOut) {
    self.blueView.center.x += 250
}

@IBAction func tappedStartButton(_ sender: Any) {
    animator.startAnimation()
}

@IBAction func tappedPauseButton(_ sender: Any) {
    animator.pauseAnimation()
}

@IBAction func tappedContinueButton(_ sender: Any) {
    animator.continueAnimation(withTimingParameters: nil, durationFactor: 3)
}

```

まず、Startボタンをタップして自然にアニメーションが終了するまでの時間はAnimatorに設定した通り`2.0`秒です。

![Dec-03-2018 18-24-52.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/9199d278-aa56-6cee-8e87-bdae26ef6f3f.gif)

次にアニメーションを開始して、一時停止させ、アニメーションを再開させた時の挙動が下記になります。

![Dec-03-2018 18-32-28.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/71e11928-db87-5657-decc-4ad96dfc2535.gif)

本来、`2.0`秒のアニメーションのはずが、Continueボタンをタップした後に実行されたアニメーションの時間が明らかに`2.0`秒よりも長くなっているのが分かります。


これは、`continueAnimation(withTimingParameters:durationFactor:)`メソッドが呼ばれた時に、Animatorの`残り時間`と、指定した`durationFactor`の値を乗算した値がアニメーション時間になるからです。
(`Animatorの残り時間 × durationFactor` = continueAnimationで実行したアニメーションの時間)


また、`parameters`引数の方に、バネ効果の詳細な設定ができる`UISpringTimingParameters`クラスやアニメーショカーブの詳細な設定ができる`UICubicTimingParameters`クラスのオブジェクトを指定することで、元々そういった効果などを設定していなかったAnimatorにも効果を反映することができます。

```swift
let parameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.11, y: 0.98), controlPoint2: CGPoint(x: 0.97, y: 0.03))
animator.continueAnimation(withTimingParameters: parameters, durationFactor: 3)
```

![Dec-03-2018 19-08-11.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/de27123e-fa23-3700-b2c3-e03f3332e150.gif)


しかしアニメーションカーブなどの設定によって`durationFactor`で指定した倍率通りのアニメーション時間になっていないかも・・? :thinking: 

上記の動画の挙動を確認してみると怪しい気がする。。


`continueAnimation(withTimingParameters:durationFactor:)`メソッドの使い所としては、ユーザーのジェスチャ操作のvelocityの値を反映した加速度や時間に変更することができるのです。

なお、`continueAnimation(withTimingParameters:durationFactor:)`によってアニメーションが完了して`inactive`になったAnimatorは、`continueAnimation(withTimingParameters:durationFactor:)`で変更した速度などは引き継がれません。
あくまでもアニメーション再開時のみの一時的な効果です。

再開したアニメーションが終了するとAnimatorに元々設定されていたアニメーション時間やバネ・アニメーション効果に戻ります。


## アニメーションの進捗率を操作する（インタラクティブアニメーション）
[fractionComplete]:https://qiita.com/hachinobu/items/bc2e1fa0c3bbd0a51d17#%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E9%80%B2%E6%8D%97%E7%8E%87%E3%82%92%E6%93%8D%E4%BD%9C%E3%81%99%E3%82%8B%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3

アニメーションの進捗率を操作することができます。
ユーザーの操作とアニメーションの進捗率を連動させることでインタラクティブなアニメーションになります。
UIViewPropertyAnimatorの**キモ**となる機能です。

### `fractionComplete`プロパティ

```swift
public var fractionComplete: CGFloat { get set }
```

アニメーションの進捗率の値で、`0.0`〜`1.0`の間で定義されます。

まず、`fractionComplete`の挙動を把握するために、`minimumValue = 0.0`, `maximumValue = 1.0`の設定がされているUISliderの値と連動させたシンプルなアニメーションコードを見てみましょう。

```swift

animator = UIViewPropertyAnimator(duration: 3.0, curve: .easeInOut) {
    self.redView.center.x = self.view.frame.width
    self.redView.transform = CGAffineTransform(rotationAngle: CGFloat.pi).scaledBy(x: 0.001, y: 0.001)
}

func sliderValueChange(_ sender: UISlider) {
    // sliderは、slider.minimumValue = 0.0, slider.maximumValue = 1.0
    // sliderの値 = アニメーションの進捗値として連動させて使う
    animator.fractionComplete = CGFloat(sender.value)
}

```

このコードは、Sliderの動きに連動してアニメーションの進捗率を変えています。

![Dec-03-2018 13-58-16.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/b8473414-5adf-b12f-a73e-e2fe12a803f5.gif)

このように、正にユーザーの操作に連動してアニメーションの進捗率を変えることができるのです。

また、Animatorの状態として、`inactive`なAnimatorに`fractionComplete`を設定すると`active`に状態遷移します。
この時`isRuning`は`false`でありアニメーションは実行されません。

`fractionComplete`でアニメーションの進捗率を操作したAnimatorに対して`startAnimation()`をすると、現在の進捗率の位置からアニメーションが実行されます。

```swift
animator = UIViewPropertyAnimator(duration: 3.0, curve: .easeInOut) {
    self.redView.center.x = self.view.frame.width
    self.redView.transform = CGAffineTransform(rotationAngle: CGFloat.pi).scaledBy(x: 0.001, y: 0.001)
}

func sliderValueChange(_ sender: UISlider) {
    animator.fractionComplete = CGFloat(sender.value)
}

func tappedStartButton(_ sender: Any) {
    animator.startAnimation()
}
```

![Dec-03-2018 14-18-46.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/c09ebc84-9d5b-9675-5e6e-162bd9e82381.gif)

`startAnimation()`が実行された時のアニメーション完了までの時間は、アニメーションの進捗率である`fractionComplete`により変わります。

アニメーション自体の時間が`3.0`秒に設定されていて、`fractionComplete`が`0.5`の時に`startAnimation()`でアニメーションを開始すると`1.5`秒で残り(`0.5`の行程)のアニメーションを完了させます。

余談ですが、UIViewPropertyAnimatorのアニメーションを`UIView.transition`メソッドと組み合わせて`fractionComplete`と連動させるのも面白いです。

```swift
animator = UIViewPropertyAnimator(duration: 1.0, curve: .easeInOut) {
    UIView.transition(with: self.redView, duration: 1.0, options: .transitionFlipFromTop, animations: nil, completion: nil)
}

@IBAction func sliderValueChange(_ sender: UISlider) {
    animator.fractionComplete = CGFloat(sender.value)
}
```

![Dec-05-2018 17-53-43.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/1b069361-a857-4a01-67e9-a45e36322104.gif)





今度はUISliderの組み合わせではなく、ユーザーのジェスチャ操作とアニメーションを連動させてみます。
ここでは、[UIViewPropertyAnimatorを使った簡単なサンプル][サンプルコード1]とは違う複数のAnimatorを使ってみたいと思います。

まずはコードの全容です。

```swift

class ViewController: UIViewController {
    
    private lazy var whiteView: UIView = {
        let view = UIView()
        view.backgroundColor = .white
        view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
        return view
    }()
    private var whiteViewTopConstraint = NSLayoutConstraint()

    private lazy var imageView: UIImageView = {
        let imageView = UIImageView()
        imageView.image = #imageLiteral(resourceName: "img")
        return imageView
    }()
    private var blurView = UIVisualEffectView()

    private var offset: CGFloat {
        return view.frame.maxY - 80
    }
    private let whiteViewFinalConstantPoint: CGFloat = 64

    private var whiteViewAnimator = UIViewPropertyAnimator(duration: 3.0, curve: .easeInOut)
    private var blurViewAnimator = UIViewPropertyAnimator(duration: 3.0, curve: .easeInOut)
    private var animators = [UIViewPropertyAnimator]()

    override func viewDidLoad() {
        super.viewDidLoad()

        imageView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(imageView)
        NSLayoutConstraint.activate(
            [
                imageView.topAnchor.constraint(equalTo: view.topAnchor),
                imageView.leftAnchor.constraint(equalTo: view.leftAnchor),
                imageView.rightAnchor.constraint(equalTo: view.rightAnchor),
                imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            ]
        )
        blurView.translatesAutoresizingMaskIntoConstraints = false
        imageView.addSubview(blurView)
        NSLayoutConstraint.activate(
            [
                blurView.topAnchor.constraint(equalTo: imageView.topAnchor),
                blurView.leftAnchor.constraint(equalTo: imageView.leftAnchor),
                blurView.rightAnchor.constraint(equalTo: imageView.rightAnchor),
                blurView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor)
            ]
        )

        whiteView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(whiteView)
        whiteViewTopConstraint = whiteView.topAnchor.constraint(equalTo: view.topAnchor, constant: offset)
        NSLayoutConstraint.activate(
            [
                whiteViewTopConstraint,
                whiteView.leftAnchor.constraint(equalTo: view.leftAnchor),
                whiteView.rightAnchor.constraint(equalTo: view.rightAnchor),
                whiteView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            ]
        )
        
        let recognizer = UIPanGestureRecognizer()
        recognizer.addTarget(self, action: #selector(self.whiteViewPanned(recognizer:)))
        whiteView.addGestureRecognizer(recognizer)
    }

    private func setupAnimator() {
        guard animators.isEmpty else {
            return
        }
        whiteViewAnimator.addAnimations {
            self.whiteViewTopConstraint.constant = self.whiteViewFinalConstantPoint
            self.whiteView.layer.cornerRadius = 200
            self.view.layoutIfNeeded()
        }
        blurViewAnimator.addAnimations {
            self.blurView.effect = UIBlurEffect(style: .light)
        }
        animators.append(contentsOf: [whiteViewAnimator, blurViewAnimator])
    }

    @objc private func whiteViewPanned(recognizer: UIPanGestureRecognizer) {
        switch recognizer.state {
        case .began:
            setupAnimator()
        case .changed:
            let translation = recognizer.translation(in: whiteView)
            animators.forEach { $0.fractionComplete = -translation.y / (offset - whiteViewFinalConstantPoint) }
        case .ended:
            animators.forEach { $0.continueAnimation(withTimingParameters: nil, durationFactor: 0) }
        default: ()
        }
    }

}

```

これを実行すると下記のような挙動になります。
（なお、下記サンプルは`fractionComplete`の挙動把握のサンプルなので一方行で終わりのサンプルです。）

![fractionsample 2018-12-05 12_04_08](https://user-images.githubusercontent.com/1317847/49487467-de606c00-f885-11e8-9e1b-045ae0eb535f.gif)


このように白いViewを上部にパンしていくにつれ、白いViewが伸びていきながら左上・右上が角丸、背景にぼかし処理がかかっていく様子が見て取れると思います。
白いViewを下方向にパンしていくと、その逆の事象になります。（元の状態に戻ろうとする）

そして指を離すと、その位置からアニメーションが実行されます。


では、ポイントとなる箇所を説明していきます。

まず、今回は下記2つのAnimatorを使っています。
* `whiteView`操作のAnimator
* `blurView`操作のAnimator
また、2つのAnimatorをまとめて使えるように`animators`という`UIViewPropertyAnimator`の配列を用意しています。

それぞれにアニメーションを設定している箇所が下記になります。

```swift
private func setupAnimator() {
    guard animators.isEmpty else {
        return
    }
    whiteViewAnimator.addAnimations {
        self.whiteViewTopConstraint.constant = self.whiteViewFinalConstantPoint
        self.whiteView.layer.cornerRadius = 200
        self.view.layoutIfNeeded()
    }
    blurViewAnimator.addAnimations {
        self.blurView.effect = UIBlurEffect(style: .light)
    }
    animators.append(contentsOf: [whiteViewAnimator, blurViewAnimator])
}
```

このアニメーションの内容は

* whiteViewAnimator
  * `whiteView`のTopAnchorの位置を現在の位置から`ViewController.view`のTopAnchor+64の位置にする
  * `whiteView`の`cornerRadius`を0から200にする 

* blurViewAnimator
  * `imageView`の上に引いている`blurView`にlightの`blur`をかける（`(style: nil)` to `(style: .dark)`）

というアニメーションを設定して配列`animators`に格納しています。


次にユーザーのジェスチャ操作とアニメーションを連動させている箇所です。


```swift

@objc private func whiteViewPanned(recognizer: UIPanGestureRecognizer) {
    switch recognizer.state {
    case .began:
        setupAnimator()
    case .changed:
        let translation = recognizer.translation(in: whiteView)
        animators.forEach { $0.fractionComplete = -translation.y / (offset - whiteViewFinalConstantPoint) }
    case .ended:
        animators.forEach { $0.continueAnimation(withTimingParameters: nil, durationFactor: 0) }
    default: ()
    }
}

```


ユーザーが指を動かした時に、その指の位置は最終的に行きたい位置のどれくらいの割合の位置なのかを取得して、その値をそのまま各Animatorの`fractionComplete`に代入しています。

`fractionComplete`が代入されたAnimatorは状態が`active`になり、自身に設定されているアニメーションが進捗率の位置にそれぞれ動きます。

このようにユーザーの操作に連動して複数のAnimatorのアニメーションを進めたり、戻したりすることができます。

そして、ユーザーが指を離した時に、Animatorに[continueAnimationメソッド][continueAnimation]を呼ぶことで、現在の進捗率の位置からアニメーションが開始されます。

#### Tips

`fractionComplete`を使うときに、Animatorの状態が`active`かつアニメーション一時停止中（`isRunning == false`）の場合は、`fractionComplete`に`0.0` もしくは `1.0`を代入してもアニメーションが完了・破棄されません。
しかし、Animatorの状態が`active`かつアニメーション実行中（`isRunning == true`）の場合では、`fractionComplete`に`0.0` もしくは `1.0`が代入された瞬間にアニメーションが完了したと見なされアニメーションが破棄されてしまうので、それ以降`fractionComplete`に進捗率を代入してもアニメーションは動きません。（厳密には`fractionComplete`に値を代入しても全て`0.0`になります。）

なので、アニメーション実行中（`isRunning == true`）で`fractionComplete`を使いたい場合は、`0.01`〜`0.99`までの範囲内の値を入れるようにすることでアニメーションの完了・破棄を阻止できます。
もしくは[pausesOnCompletionプロパティ][pausesOnCompletion]を使います。


## Animatorの割り込み操作を無効にする

### `isInterruptible`プロパティ

Animatorが割り込み可能なオブジェクトかを表します。
（`pauseAnimation()`や`stopAnimation(_:)`を使ってAnimatorの状態変更を許可するかどうか。）

デフォルトでは`true`です。

このプロパティを`false`にすると`startAnimation()`でアニメーションが実行されてから完了するまで、Animatorに対する変更を受け付けません。

`isInterruptible`が`false`の時にAnimatorを操作するメソッドを呼び出すとクラッシュします。

また、Animatorの状態が`inactive`の時にだけ`isInterruptible`の値を変更できます。
`inactive`以外の状態で`isInterruptible`に変更を加えるとクラッシュします。

インタラクティブなアニメーションを実現するには当然`true`で使います。


## アニメーションが終了してもアニメーションを破棄しない

* 正常にアニメーションが完了したらアニメーションを破棄せずに`active`状態を保ち続けることができます。
（ここでいうアニメーションが正常に完了とは`stopAnimation(:)`メソッドなどで明示的にアニメーションを終了させたのでなく、実行中のアニメーションが最後まで完了したことをさします）

### `pausesOnCompletion`プロパティ

[pausesOnCompletion]:https://qiita.com/items/bc2e1fa0c3bbd0a51d17#pausesoncompletion%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3

iOS11から追加されたプロパティです。

`true`にすると設定したアニメーションが正常に完了してもAnimatorの状態は`active`をキープします。
この時、アニメーション終了後の処理である`completion`ブロックは呼ばれません。

`active`状態がキープされるということは、アニメーションが完了してもAnimatorに設定されているアニメーションは破棄されません。
なので、アニメーションが完了したAnimatorにそのまま`isReverse = true`を指定して、`startAnimation()`することで、再びアニメーションが実行されます。

下記で確認できます。

```swift

animator = UIViewPropertyAnimator(duration: 2.0, curve: .easeIn) {
    self.blueView.center.x += 200
}
animator.pausesOnCompletion = true

// pausesOnCompletionがtrueなので、ここは呼ばれない
animator.addCompletion { _ in
    self.blueView.alpha = 0.0
}

@IBAction func tappedReverseStartButton(_ sender: Any) {
    if animator.isRunning { return }
    animator.isReversed = true
    animator.startAnimation()
}

@IBAction func tappedStartButton(_ sender: Any) {
    animator.isReversed = false
    animator.startAnimation()
}

```

![Dec-04-2018 18-17-31.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/900016e9-154e-be4e-80f6-2d4fab070df7.gif)

このようにAnimatorが`active`状態をキープするので、アニメーション完了後にアニメーションの再設定が不要になります。

`completion`ブロックが呼ばれないので、アニメーションが完了したかどうかは`isRunning`プロパティで判断します。
アニメーションが完了している場合は`active`状態で`isRunning`が`false`のAnimatorになります。


このプロパティを`true`で使うことで[UIViewPropertyAnimatorを使った簡単なサンプル][サンプルコード1]で記述したコードをより簡潔にすることができます。

まず、Animatorの設定を`viewDidLoad`で定義するように変更します。
そして、`setupAnimator`メソッドと`ImageViewState`は使わないので削除してしまいます。

```swift
//viewDidLoad()に追加
animator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut) {
    self.blurView.effect = UIBlurEffect(style: .light)
    self.imageViewTopConstraint.constant = -self.offset
    self.view.layoutIfNeeded()
}
animator.pausesOnCompletion = true
```

そして、　`imageViewPanned(recognizer:)`メソッドを下記のように書き換えます。

```swift

@objc private func imageViewPanned(recognizer: UIPanGestureRecognizer) {
    switch recognizer.state {
    case .began:
        animator.pauseAnimation()
        animationProgress = animator.fractionComplete
    case .changed:
        let translation = recognizer.translation(in: imageView)
        var fraction = -translation.y / offset
        if animator.isReversed { fraction *= -1 }
        animator.fractionComplete = fraction + animationProgress

    case .ended:
        let yVelocity = recognizer.velocity(in: imageView).y
        if yVelocity == 0 {
            animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
            return
        }
        animator.isReversed = (yVelocity > 0)
        animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)

    default: ()
    }
}

```

これでアプリケーションを実行すると、[UIViewPropertyAnimatorを使った簡単なサンプル][サンプルコード1]と同じ挙動になると思います。

修正前は現在の値が`close`なのか`open`なのかを判定して、毎度Animatorに適切なアニメーションを追加していましたが、ここでは`pausesOnCompletion`を使って単方向のアニメーションを1回のみ設定し、逆方向への実行は`isReverse`プロパティを使うようにしています。

`pausesOnCompletion`を`true`で使う場合には、`isRunning`と`isReverse`プロパティの組み合わせで状態を把握することが大事です。


なお、`pausesOnCompletion`が`true`であっても、Animatorに`stopAnimation(_:)`をするとAnimatorの状態は`inactive`や｀stopped`に遷移します。
この場合はAnimatorのアニメーションは破棄されます。

あくまでも自然にアニメーションが完了した場合に`active`状態をキープするためのプロパティです。


## インタラクティブアニメーションでもアニメーションカーブを適用する

### `scrubsLinearly`プロパティ

iOS11から追加されたプロパティで、このプロパティは`fractionComplete`を使ってアニメーションの進捗率を変えて操作する際に、そのアニメーションに適用されるアニメーションカーブを直線的な`linear`にするかどうかです。

デフォルトは`true`で、`fractionComplete`でのアニメーションカーブは`linear`が適用されます。

これを`false`にすることでAnimatorに設定されたアニメーションカーブが反映されます。

下記で挙動を把握してみましょう。

```swift
animator = UIViewPropertyAnimator(duration: 3.0, controlPoint1: CGPoint(x: 0.11, y: 0.98), controlPoint2: CGPoint(x: 0.97, y: 0.03)) {
    self.redView.center.x = self.view.frame.width
    self.redView.transform = CGAffineTransform(rotationAngle: CGFloat.pi).scaledBy(x: 0.001, y: 0.001)
}
animator.scrubsLinearly = false // true or false

@IBAction func sliderValueChange(_ sender: UISlider) {
    animator.fractionComplete = CGFloat(sender.value)
}   
```

|scrubsLinearly == true|scrubsLinearly == false|
|:--:|:--:|
|![Dec-04-2018 19-42-40.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/15662306-276c-2489-d1c9-1b2359450059.gif)|![Dec-04-2018 19-42-12.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/a0907b17-9765-73dc-6f4c-303a2f96b382.gif)|

このように`scrubsLinearly`を`false`に設定することでAnimatorに設定されているアニメーションカーブが適用されます。

# UIViewPropertyAnimatorを駆使して作ったもの

## HalfModal

UIViewPropertyAnimatorを組み合わせたら、もしや巷で話題のショートカットアプリなどで使われているセミモーダルみたいな挙動も簡単につくれるのでは？:thinking:  と思い簡易なサンプルを作ってみました。

![HalfModal.gif](https://qiita-image-store.s3.amazonaws.com/0/27785/1b3cd0e3-b2ca-120b-a457-12a4fc1c9b0b.gif)


まだ、やっつけ感のあるコードですが、UIViewPropertyAnimatorの状態や機能を組み合わせて実現しています。
（実際には全然簡単には出来ず無茶苦茶ハマりました・・。その知見が本記事に繋がっています。）

UIViewPropertyAnimatorの制御を色々やっているので、もし良ければコードも覗いてみてください。

https://github.com/hachinobu/HalfModal


# 終わりに

いかがだったでしょうか。

今回は例に出していませんが、画面遷移とUIViewPropertyAnimatorを組み合わせる事でインタラクティブな画面遷移が作れたり、面白いことが色々できます。

この記事を通じてUIViewPropertyAnimatorを使ってみようとか、試してみようとか思ってもらえたら嬉しいです。

明日は [@ken5scal](https://twitter.com/ken5scal) さんです。

# 参考文献
https://developer.apple.com/documentation/uikit/uiviewpropertyanimator
https://medium.com/@nathangitter/building-fluid-interfaces-ios-swift-9732bb934bf5
http://www.swiftkickmobile.com/building-better-app-animations-swift-uiviewpropertyanimator/