[Swift] Pinterest風のカスタムトランジションを実装してみる

# はじめに
今更ですが、
UIViewControllerTransitioningDelegateを利用した
カスタムトランジションのサンプルを備忘録として残しておきます。

動作イメージは、Pinterest風のトランジションです。
素敵なライブラリもありますが、勉強がてらに実装してみます。

![circleAnimationMuvie.gif](https://qiita-image-store.s3.amazonaws.com/0/71694/8327845d-f258-1ec7-0922-1a724a18e9d0.gif)

# 実装手順
1. 画面遷移元、画面遷移先、CollectionViewCell用のプロトコルを定義する
2. 画面表示時のアニメーションクラスを定義する
3. 画面非表示時のアニメーションクラスを定義する
4. UIViewControllerTransitioningDelegateを継承したサブクラスを作る
5. Pinterest風アニメーションを利用してみる

## 1. 画面遷移元、画面遷移先、CollectionViewCell用のプロトコルを定義する
トランジションに必要なプロパティをプロトコルで定義します。
今回は、下記の３つのプロトコルを定義します。

・画面遷移元には、UICollectionViewと土台のUIViewを設置します
・画面遷移先には、UIImageViewと土台のViewを設置します
・CollectionViewCellには、UIImageViewを設置します

```swift:ImageTransitionProtocol.swift
import UIKit

protocol ImageSourceTransitionType {
    var collectionView: UICollectionView! {get}
    var baseView: UIView! {get}
}

protocol ImageDistinationTransitionType {
    var imageView: UIImageView! {get}
    var baseView: UIView! {get}
}

protocol ImageCollectionViewCellType {
    var imageView: UIImageView! {get}
}
```

## 2. 画面表示時のアニメーションクラスを定義する
画面表示時のアニメーションクラスをImagePresentedAnimatorとします。

```swift:ImagePresentedAnimator.swift
import UIKit

class ImagePresentedAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 0.3
    }

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

        guard let sourceVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
            as? ImageSourceTransitionType else {
            return
        }

        guard let distinationVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
            as? ImageDistinationTransitionType else {
            return
        }

        guard let containerView = transitionContext.containerView() else {
            return
        }

        let animationDuration = transitionDuration(transitionContext)

        guard let selectedIndexPath = sourceVC.collectionView.indexPathsForSelectedItems()?.first else {
            return
        }

        let selectedCell = sourceVC.collectionView.cellForItemAtIndexPath(selectedIndexPath)
            as! ImageCollectionViewCellType
        let selectedImage = selectedCell.imageView.image

        let selectedCellsFrame = containerView.convertRect(selectedCell.imageView.frame,
            fromView: selectedCell.imageView.superview)

        let selectedImageWrapperView = UIView.init(frame: selectedCellsFrame)
        selectedImageWrapperView.backgroundColor = UIColor.clearColor()
        selectedImageWrapperView.clipsToBounds = true

        let imageView = UIImageView.init(image: selectedImage)
        imageView.frame = CGRectMake(0.0, 0.0, CGRectGetWidth(selectedCellsFrame), CGRectGetHeight(selectedCellsFrame))
        imageView.contentMode = distinationVC.imageView.contentMode
        imageView.autoresizingMask = [.FlexibleHeight,.FlexibleWidth,.FlexibleTopMargin,.FlexibleBottomMargin]
        selectedImageWrapperView.addSubview(imageView)

        containerView.addSubview(selectedImageWrapperView)

        let whiteBackgroundView = UIView.init(frame: sourceVC.baseView.frame)
        whiteBackgroundView.backgroundColor = UIColor.whiteColor()
        containerView.insertSubview(whiteBackgroundView, belowSubview: selectedImageWrapperView)

        distinationVC.baseView.alpha = 0.0
        containerView.addSubview(distinationVC.baseView)

        let rect = CGRectMake(0.0, 0.0,
                              CGRectGetWidth(distinationVC.baseView.frame),
                              CGRectGetHeight(distinationVC.baseView.frame))
        let imageViewFinalFrame = containerView.convertRect(rect, fromView: distinationVC.baseView)

        UIView.animateWithDuration(animationDuration,
                                   delay: 0.0,
                                   options: .CurveEaseOut,
                                   animations: {
                                    selectedImageWrapperView.frame = imageViewFinalFrame
            }) { (finished) in

                distinationVC.imageView.image = selectedImage;
                distinationVC.baseView.alpha = 1.0;
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
        }
    }
}
```

## 3. 画面非表示時のアニメーションクラスを定義する
画面非表示時のアニメーションクラスをImageDismissedAnimatorとします。

```swift:ImageDismissedAnimator.swift
import UIKit

class ImageDismissedAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 0.3
    }

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

        guard let sourceVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
            as? ImageDistinationTransitionType else {
                return
        }

        guard let distinationVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
            as? ImageSourceTransitionType else {
                return
        }

        guard let containerView = transitionContext.containerView() else {
            return
        }

        let animationDuration = transitionDuration(transitionContext)

        let snapshot = sourceVC.imageView.snapshotViewAfterScreenUpdates(false)
        snapshot.frame = containerView.convertRect(sourceVC.imageView.frame,
                                                   toView: sourceVC.imageView.superview)
        snapshot.autoresizingMask = [.FlexibleHeight,.FlexibleWidth,.FlexibleTopMargin,.FlexibleBottomMargin]
        sourceVC.imageView.alpha = 0.0

        guard let selectedIndexPath = distinationVC.collectionView.indexPathsForSelectedItems()?.first else {
            return
        }

        let selectedCell = distinationVC.collectionView.cellForItemAtIndexPath(selectedIndexPath) as! ImageCollectionViewCellType
        selectedCell.imageView.alpha = 0.0
        selectedCell.imageView.transform =  CGAffineTransformMakeScale(1.1, 1.1)

        distinationVC.baseView.frame = transitionContext.finalFrameForViewController(distinationVC as! UIViewController)
        containerView.insertSubview(distinationVC.baseView, belowSubview: sourceVC.baseView)

        let imageWrapperView = UIView.init(frame: containerView.convertRect(sourceVC.imageView.frame,
            toView: sourceVC.imageView.superview))
        imageWrapperView.clipsToBounds = true
        imageWrapperView.addSubview(snapshot)
        containerView.addSubview(imageWrapperView)

        let whiteBackgroundView = UIView.init(frame: sourceVC.baseView.frame)
        whiteBackgroundView.backgroundColor = UIColor.whiteColor()
        containerView.insertSubview(whiteBackgroundView, belowSubview: imageWrapperView)

        UIView.animateKeyframesWithDuration(animationDuration,
                                            delay: 0.0,
                                            options: UIViewKeyframeAnimationOptions.CalculationModeLinear,
                                            animations: {

                                                UIView .addKeyframeWithRelativeStartTime(0.0,
                                                    relativeDuration: 1.0,
                                                    animations: {

                                                        sourceVC.baseView.alpha = 0.0
                                                        imageWrapperView.frame = containerView.convertRect(selectedCell.imageView.frame,
                                                            fromView: selectedCell.imageView.superview)
                                                        whiteBackgroundView.alpha = 0.0
                                                        selectedCell.imageView.alpha = 1.0
                                                })

                                                UIView .addKeyframeWithRelativeStartTime(0.95,
                                                    relativeDuration: 0.05,
                                                    animations: {
                                                        selectedCell.imageView.transform = CGAffineTransformIdentity
                                                        snapshot.alpha = 0.0
                                                })

            }) { (finished) in

                whiteBackgroundView.removeFromSuperview()
                snapshot.removeFromSuperview()
                imageWrapperView.removeFromSuperview()
                sourceVC.imageView.hidden = false
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
        }
    }
}
```
## 4. UIViewControllerTransitioningDelegateを継承したサブクラスを作る

```swift:ImageTransitionDelegate.swift
import UIKit

class ImageTransitionDelegate: NSObject {}

extension ImageTransitionDelegate: UIViewControllerTransitioningDelegate{

    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return ImagePresentedAnimator()
    }

    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return ImageDismissedAnimator()
    }
}
```

## 5. Pinterest風アニメーションを利用してみる

### 画面イメージ

<img width="726" alt="スクリーンショット 2016-09-06 21.16.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/4348592f-675c-9c4f-d46f-f0dae8c9b7e1.png">

#### 画面遷移元画面

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet var baseView: UIView!

    var imageTransitioningDelegate: ImageTransitionDelegate?
}

extension ViewController: UICollectionViewDataSource {

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("ImageCollectionViewCell",
        forIndexPath: indexPath) as! ImageCollectionViewCell

        //適当に画像を表示しています
        let imageName = indexPath.row % 2 == 0 ? "image1" : "image2"
        cell.imageView.image = UIImage.init(named: imageName)
        return cell
    }

    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        //適当に表示する画像数返しています
        return 15
    }
}

extension ViewController: UICollectionViewDelegate {

    func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {

        if imageTransitioningDelegate == nil {
            imageTransitioningDelegate = ImageTransitionDelegate()
        }

        let vc = UIStoryboard(name: "Main",bundle: nil).instantiateViewControllerWithIdentifier("DetailViewController")
            as! DetailViewController

        //Pinterest風のトランジションをセットしています
        vc.transitioningDelegate = imageTransitioningDelegate

        let imageName = indexPath.row % 2 == 0 ? "image1" : "image2"
        vc.imageToPresent = UIImage.init(named: imageName)
        presentViewController(vc, animated: true, completion: nil)
    }
}


extension ViewController: ImageSourceTransitionType {}
```

#### 画面遷移先画面

```swift:DetailViewController.swift
import UIKit

class DetailViewController: UIViewController {

    var imageToPresent: UIImage!
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet var baseView: UIView!

    @IBAction func didTappedDissmiss(sender: UIButton) {
        self.dismissViewControllerAnimated(true, completion: nil)
    }
}

extension DetailViewController: ImageDistinationTransitionType {}

```

#### CollectionViewCell

```swift:ImageCollectionViewCell.swift
import UIKit

class ImageCollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var imageView: UIImageView!
}

extension ImageCollectionViewCell: ImageCollectionViewCellType {}
```

# まとめ
基本的には、UIViewControllerAnimatedTransitioningを継承したクラスでアニメーションを定義し、
UIViewControllerTransitioningDelegateを継承したクラスで、各アニメーションと関連付けます。

あとは、画面遷移時に、transitioningDelegateのプロパティに上記のクラスをセットするだけです。

## 参考
http://zappdesigntemplates.com/create-custom-uicollectionviewcell-transition/