UIViewPropertyAnimatorを用いて独自の画面遷移を実装する

この記事は[OthloTech Advent Calendar 2016](http://qiita.com/advent-calendar/2016/othlotech)の16日目の記事です。

<br>

iOS10から利用可能なUIViewPropertyAnimatorクラスを用いることでアニメーションに対して柔軟に対応できるようになりました。
今回はそれを用いて画面遷移を自作してみました。

自分のメモとして少し事細かく書いている部分があるので、さらっと読みたい方は最初のアニメーションの実装例と最後のアニメーションを用いた画面遷移の部分を読むといいかと思います。

# 開発環境
Xcode 8.1
Swift 3.0.1

# アニメーションの実装例
まずは簡単なアニメーションの実装を行いたいと思います。
以下が基本的なアニメーションを行う部分になります。

```Swift:
let timing: UITimingCurveProvider = UICubicTimingParameters(animationCurve: .easeIn)
let animator = UIViewPropertyAnimator(duration: 2.0, timingParameters: timing)  // 2秒かけて

animator.addAnimations {
    // アニメーションの処理
}

animator.addCompletion {_ in
    // アニメーション終了時の処理
}

animator.startAnimation()  // アニメーションを開始する
```

UITimingCurveProviderでアニメーションのタイミングを設定することができます。

| 種類       |              説明 |
|:----------:|:----------------:|
| easeIn     | 最初がゆっくり      |
| easeInOut  | 最初と最後がゆっくり |
| easeOut    | 最後がゆっくり      |
| linear     | 一定の速度        |

また、以下のように書くことで自分でイージングの設定を行えます。

```swift:
let c1: CGPoint = CGPoint(x:0.0, y:0.7)
let c2: CGPoint = CGPoint(x:1.0, y:0.3)
let timing = UICubicTimingParameters(controlPoint1: c1, controlPoint2: c2)
```

<br>

そして、UIViewPropertyAnimatorを用いることでアニメーションに対する様々なアプローチが行えます。

実装例を書いてみました。

```Swift:ViewController.swift
import UIKit

class ViewController: UIViewController {
    
    var squareView: UIView!
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // UIView
        squareView = UIView(frame: CGRect(x:0, y:350, width:50, height:50))
        squareView.backgroundColor = UIColor.red
        self.view.addSubview(squareView)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        let timing: UITimingCurveProvider = UICubicTimingParameters(animationCurve: .easeIn)
        let animator = UIViewPropertyAnimator(duration: 2.0, timingParameters: timing)  // 2秒かけて

        // アニメーション
        animator.addAnimations {
            self.squareView.center = CGPoint(x: self.view.frame.width-25, y: self.squareView.center.y)
            self.squareView.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI))  // 180度回転
        }
        
        // アニメーション後
        animator.addCompletion {_ in
            self.squareView.backgroundColor = UIColor.orange
        }
        
        animator.startAnimation()  // 開始
    }
    
    
}
```

このように書くと以下のようになります。

![squareViewAnimation](https://lh3.googleusercontent.com/UadVpON1721eULM6D96RmP0WSccyaiKpcV4McU1Zcix9jos_kpFTc6F__JvkH8_KcY57Rij9CXi8tps1RebYOvXEiIxY3XYsBqOCE5C-H0YlGELbM8DaHOOciow9sD03xBJITx42H3StKzOUZlZcKkIjJR4FsUCirJKRxzXUh7CJpGjBBt1wiPmn8Z7hiShjF6-CjobbVs9o4YWVTOX4_0DZNe6nixdwjS2eB5DKhutusfpryIghjm1cQsnAVvmLj6NERWj5b9r_H57wjPdPvBINz7a_oVw0i20PIp30r87lUN_XGoyjLFWBUDUtlJhvIQMzxKfFY0OMEWkXbrb7sjcbCr-L8t7guCxT8O0-zIZdpfZjzOiHKSZvdZQb4vWhhW5ZWRePs8UuKx_Yj7HNVKsh9hwvVySr19g1E8oKhotx6zOm7F8XowibRutYjNIkY4T1jwUoaeSRDZFFtW-QQz6AIsA9uVvMQvRhB3c2tlB-Z8eTi-bLB6c4a2PHTLKai1xJ3KrLjfRhu970tPqlBGUjNq5aj-msf_h0WN5PCky39CE-1ZTqmKv6_NLQUwaGl8T3bZLnSlQ5t9M5gCW2KzlgFSm0zQMxEa4MxFUDowFDy76HtUjR=w294-h545-no)

# 自作の画面遷移を実装する
次は先ほどのアニメーションの作り方を元に画面遷移を実装していきます。

## とりあえず画面遷移をコードで行う
下準備としてボタンを押したら画面遷移（ViewController.swift -> SecondViewController.swift）するコードを書いていきます。

<br>

```Siwft:UIButton
let button = UIButton()
button.setTitle("next", for: .normal)
button.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
button.center = self.view.center
button.backgroundColor = UIColor.orange
button.setTitleColor(UIColor.white, for: .normal)
button.addTarget(self, action: #selector(touchButton(sender:)), for: .touchUpInside)
self.view.addSubview(button)
```

UIButtonを作成したら、押されたときに下記メソッドを呼んでSecondViewControllerに遷移します。

```Siwft:
func touchButton(sender: UIButton) {
    let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
    let nextView = storyboard.instantiateViewController(withIdentifier: "second") as! SecondViewController
    
    present(nextView, animated: true, completion: nil)
}
```

このときに遷移がうまく行かない方は、Main.storyboardで以下を確認してください。

- ViewControllerに Is Initial View Controller のチェックがついていない
- SecondViewControllerのIdentityの Storyboard ID を書いていない（今回はIDを second とする）

<br>

次にSecondViewControllerからViewControllerに戻るときですが、これは先ほどと同様にボタンが押されたときのメソッドを作り、その中に以下の一文を入れるだけで戻れます。

```swift:
self.dismiss(animated: true, completion: nil)
```

<br>

これで画面遷移を行える状態になりました。

![defaltAnimation](https://lh3.googleusercontent.com/Vi-9n9Q_ObnX2wVydauUbQWC5LmcSWzxUJp66n88KfiY1l6Iy2d7Wc37zVXzvjdmwFFXfZCuYbTUOodWfudRP90QSLeode1dMNpd0JFymM2PSnePJYrDN1My7a0LD6SbmH-ly4geyJk05L__0vc1RbZRVMSCXkcg8KnOitWvHG_VIxRjzYyLRKBPWyZ38LvBfYNr9V9yFyn5INoskr5bRdZI3ik3r7pjB8AUPnYYOL-lexN-F6rmHzgZXYv5n1l9D0ltL4-ztj02t1dExSHYrwXd43Dl0P-8dvt3h-XE-w7LtrDvN1cPPS8WpEzeKgQaljgDWhqPI1Ge6houSpXU3E-zoEoo5N4u5jEhsTcXUZxnZZQK6lLSixdIK_RT2kVNkERLkA3CCHCHuO8-s8A2qREHzzr4-eTKob3AEt5aKgsqzNBp1axhqZPbBOro3XdeTqG3qwQvDs5Bvhmhw2LiCN0NtpjEUuTqYK16Fc_VJZY1Un9WjunOx89t8-W-LG3O8gejeje27rSn6GwnDFN8acvplqWMNRC7cuHOa4oiv9TdEvtrurCQN7FIyDNNEinMz3Dtb9YIJlrChjjrY2OTkCq1xRT-_RSMp3RJG1lGPUOXzSxQOUFW=w295-h545-no)

しかし、まだ遷移の仕方がデフォルトの状態なのでここから独自の画面遷移を実装していきます。

## アニメーションを用いた画面遷移

Animatorクラスを作っていきます。
このクラスに先ほど紹介したアニメーション処理を書いていきます。

通常は、UIViewControllerTransitioningDelegateを加えた時に以下の２つのメソッドでアニメーション処理を行います。

```swift:
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 2.0  // 遷移する時間
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        // アニメーション処理
}
```

今回は、UIViewPropertyAnimatorを使用したいため、 interruptibleAnimatorメソッドを用いります。
実際に書いてみるとこのような感じになります。

```swift:Animator.swift
import UIKit

class Animator: NSObject, UIViewControllerAnimatedTransitioning {
    
    let animationDuration = 1.0
    
    
    /**
     * 遷移時間を返す
     */
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return animationDuration
    }
    
    /**
     * アニメーションを開始する処理
     */
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        self.interruptibleAnimator(using: transitionContext).startAnimation()
    }
    
    /**
     * UIViewPropertyAnimatorを返す
     */
    func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
        let timing: UITimingCurveProvider = UICubicTimingParameters(animationCurve: .linear)
        let animator = UIViewPropertyAnimator(duration: animationDuration, timingParameters: timing)
        
        animator.addAnimations {
        }
        
        animator.addCompletion {_ in
            transitionContext.completeTransition(true)
        }
        
        return animator
    }
    

}
```

<br>

もちろん遷移時にアニメーションを呼びたいので**ViewControllerにもUIViewControllerTransitioningDelegateを定義**して、delegateの設定をしたらanimationControllerメソッドを呼べるようにします。

```swift:ViewConrtoller.swift
/**
 * buttonが押された時の処理
 */
func touchButton(sender: UIButton) {
    let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
    let nextView = storyboard.instantiateViewController(withIdentifier: "second") as! SecondViewController
    
    nextView.transitioningDelegate = self  // delegate設定
    
    present(nextView, animated: true, completion: nil)
}

/**
 * 遷移時のアニメーション処理
 */
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return animator
}

/**
 * 戻り時のアニメーション処理
 */
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return animator
}
```

遷移時と戻り時でアニメーションの処理を分けたい時は、例えばBool型の変数を用意しておいて、animationControllerの中でどちらかをtrue、falseにして、それぞれのアニメーション処理をAnimatorクラス内で書けばいいかと思います。

<br>
<br>

以上を踏まえて実際に作ってみたものがこちらです。

![slideTransitionAnimation](https://lh3.googleusercontent.com/8JogmztP31P4wzMaq0PFOQcT2JHNCSOq_vq7lvCVKvf5lg0xdmaaJBg7hwy6Uo4kpu4DoxqN7_78UwB8hv3P1e3JLqK1xGXzrOR0TRAoun9EYhiyr__STubmkOq7A7TzXjKHV6RD7eXKAG7QxOvalhow4TSc7atdt2-uEKZQf928ZeQxwDSoXVtRVhSleYHYMwenXDJwHDbr3RwFHzQXyTRLI8-oQZbm_928dYbp8D61ML3xL__ysicpHt-mmBf9S2rS6H5nvJ4jXA1P4I8Z_lXQIw5tm93iEE_xM5uz83frNqfZQqJXHEjdVMXUxVyv_8JdVajcUfmapUCGlEByYfQwVsJVjocNP4w8t45X6IoZDM7KdYqjK6maLp6eG2y4-HE-l0JGJWW-g-G0PL1j5SN4KrBDnLXpTH2zg0H4oCnWJwrt-CtAltHzBSyJ8DXfFhq74J4la4wsYrUL1ijduUPz3qhw4HkuR4HLcaU8t9PyV3eAZCSuE4_eE_pjfz3aXqYT8Bx69gPm_oJ2tjJvgrLe2PgQK-VqV0JCiUpUkpMtAEXQxeE8kY5IjmqRsXwYpMwUvqhH09oZ5IeXAkpnPt-w2xuwkEG4oi_Mznh9WDyQBlog0SOf=w293-h545-no)

<br>

Animatorクラスはこのように実装しています。

```swift:Animator.swift
import UIKit

class Animator: NSObject, UIViewControllerAnimatedTransitioning {
    
    let animationDuration = 0.5
    var transition: Bool = false
    
    
    /**
     * 遷移時間を返す
     */
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return animationDuration
    }
    
    /**
     * アニメーションを開始する処理
     */
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        self.interruptibleAnimator(using: transitionContext).startAnimation()
    }
    
    /**
     * UIViewPropertyAnimatorを返す
     */
    func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
        let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
        
        let fromView = fromVC?.view  // 元のview
        let toView = toVC?.view  // 遷移先のview
        
        let containerView = transitionContext.containerView
        containerView.addSubview(fromView!)
        containerView.addSubview(toView!)
        
        let timing: UITimingCurveProvider = UICubicTimingParameters(animationCurve: .linear)
        let animator = UIViewPropertyAnimator(duration: animationDuration, timingParameters: timing)
        
        if transition {
            // ----------遷移時----------
            // 画面右側に遷移先のviewをセットする
            toView?.frame = CGRect(x:containerView.frame.size.width,
                                   y:0,
                                   width:containerView.frame.size.width,
                                   height:containerView.frame.size.height)
            
            // アニメーション処理
            animator.addAnimations {
                fromView?.alpha = 0.0
                fromView?.frame = CGRect(x:-containerView.frame.size.width,
                                         y:0,
                                         width:containerView.frame.size.width,
                                         height:containerView.frame.size.height)
                toView?.frame = containerView.frame
            }
        } else {
            // ----------戻り時----------
            // アニメーション処理
            animator.addAnimations {
                toView?.alpha = 1.0
                fromView?.frame = CGRect(x:containerView.frame.size.width,
                                         y:0,
                                         width:containerView.frame.size.width,
                                         height:containerView.frame.size.height)
                toView?.frame = containerView.frame
            }
        }
        
        // アニメーション終わりの処理
        animator.addCompletion {_ in
            transitionContext.completeTransition(true)
        }
        
        return animator
    }
    

}
```

アニメーションの内容は、このような流れです。

1. 遷移先の画面を右側にセットする
2. 遷移したときに画面中央に移動。同時に元の画面を画面左に移動しながらalpha値を下げて徐々に暗くする
3. 戻るときは1, 2と逆のことをする

<br>

このように書くだけでとても簡単にアニメーションを用いた画面遷移を行うことができました。
これらを参考に自分なりの画面遷移を作って楽しんでみてはいかがでしょうか:upside_down:

# おまけ

![springAnimation](https://lh3.googleusercontent.com/PilHooDmTq69mRE8m1HXYhtZTOIB-YAkYJcr00mnlzDHKIlNN3GnRAU3vSzSx-3gke0firYXHExVq5EvOYWVXB7gHrsfGwlNLlhwKhM6Ve46LPCsqWy0GtswT4VeSv-xwv7jF3cmj08KfwfmTK-cfJrXfyIYW3jKyJ6FTK4PDeMuXntywqmfgUw1fBBFRZ1mdgd1CQKCWbIE2O43c3hVjvqZjgg1cGlTV5Oj1FO-QqwgqK3U-m22fUAYh1fKqDE3RNuaGC3kS3x45jszDSGe8aBxYPU9BWJf_DGoD8m-WQnK0OQ3n0XeibdsMwJYBrt43TbYbFt_MX82k-MF0zDre8_SItl07HApMTJgYgLve1j7X2iYJG5QeoB9YpOU7s5w0JHvrfSDwIKiPhkLaYQI3FZggdmFlcerIysPxoKjXHDM0m8etii-p2KqSBwWT-JsB4Se-hsdG8FoSwLXJHEXjPK2RhA8EUXBsqtX-SK1aHfMOVbo93iaFekPCQLqJpOWQ6SJX4sHmyNk7cSNiQ17KmUSOSnzvRgbMh1gifnOtlPReMYd6TEfdJ60t2-xh__ozcB0vPFxMTwhV3t0IjlDAApZmboT-rGvDrqfRh7QFdaQErAjqJk9=w293-h545-no)

以下のパラメータを用いるとバネのようなアニメーションを実装することができます。

```swift:
let springParameters: UISpringTimingParameters = UISpringTimingParameters(dampingRatio: 0.3, initialVelocity: CGVector.zero)
```

- dampingRatio　：　減衰比（値が0.0から1.0に大きくなるほど揺れが弱くなる）
- initialVelocity　：　初期速度

<br>

これらを組み合わせるとこのような画面遷移が作れたりします！

![springTransitionAnimation](https://lh3.googleusercontent.com/1T59XkNMdD_Qh1IT7wHBtR-b_7JDb_fkHR4jWjE6k8pDxBen3D0mQDzsIujpP9xxign60BE47EX4m5J36c67e2MhlMw_fL3Fi5jKO91bKxvTuilWfy_CP2Hvd1mYePj4wNK1h9tE0FY9Fjlf0lehATH8pDz4sSaOPy80dCwtwuxt4a9mTsTf64cNcWa1gMqz0vw4uHMJwxt42av0Qn19tlkLgq3jYEozdpskeI1BpdAcqqbkr1qChpTwIkei4DTx9iCpdn3qFXzYRhOQjqBeJB76GkVRkrTC2L2iKDJIItmr3gg_QEDfzZRPoKfeUsi948n5Vl4AbPOxy2vvwGbEHNkUi66hCjlkShc2KCMzZdc_YHHYWOaRYArT405eBD3fplr49rs-TaY_Q0gBarZaOFZyk9EirXnk5O2emoCvB30Stkz4Ossud-hk7U2Gb-8k3vatTj8ymcvALi5QBw5SJ7J2dxQMbiyzFIwjqIX9K6wTorrkEknnMCObn1HGiRoRuRWKAKtgVCTQv49Mb65aeo26k_MyeHnTHawGugyJ1gw6XMTvj_Hx_AO0e6Z_-aPMckkAZQtsB4r8JwHhqT13SLmhEmjRAuJDpnRWN0cwZDlEVh5BtED2=w294-h545-no)

<br>

# おわりに
自分でアニメーションから作ろうとするとすごく大変そうだなと思っていましたが、UIViewPropertyAnimatorなどを用いることで簡単に実装できました。
また、普段はMain.storyboardを使用して書いているところをコードでかいたので、そちらも勉強になりました。
やりすぎはよくないですが、シンプルな遷移に飽きたという方はこれを機にオリジナルの画面遷移を作って楽しんでみてください！

最後に作ったおまけは[Github](https://github.com/KosukeGit/AnimateTransition)に載せておくので、良ければ参考にしてください:laughing: